diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
new file mode 100644
index 0000000..563b87d
--- /dev/null
+++ b/.github/workflows/ruff.yml
@@ -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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b1a1554..299b649 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 1c223fa..40d4e71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,5 +132,7 @@ dmypy.json
config.json
debug
-/static/admin/
-/static/fontawesomefree/
+local-cdn/
+config.json
+node_modules/
+package-lock.json
diff --git a/FOSSDB_web/urls.py b/FOSSDB_web/urls.py
deleted file mode 100644
index 4025788..0000000
--- a/FOSSDB_web/urls.py
+++ /dev/null
@@ -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),
-]
diff --git a/README.md b/README.md
index 7f0e479..da9efdf 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/config.json b/config.json.example
similarity index 68%
rename from config.json
rename to config.json.example
index dcaea64..d834f37 100644
--- a/config.json
+++ b/config.json.example
@@ -1,10 +1,9 @@
{
+ "DEBUG": false,
"SECRET_KEY": "",
- "ALLOWED_HOSTS": [
- "",
- "localhost"
- ],
+ "ALLOWED_HOSTS": [""],
"DATABASE": {
+ "ENGINE": "",
"NAME": "",
"USER": "",
"PASSWORD": "",
diff --git a/pyproject.toml b/pyproject.toml
index 53ca1a8..f7a8176 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 4e4add1..fc6e18e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 14597cb..64c40bd 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -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
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 7f1a176..0000000
--- a/setup.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from setuptools import setup
-
-if __name__ == "__main__":
- setup()
diff --git a/FOSSDB_web/__init__.py b/src/FOSSDB/__init__.py
similarity index 100%
rename from FOSSDB_web/__init__.py
rename to src/FOSSDB/__init__.py
diff --git a/FOSSDB_web/asgi.py b/src/FOSSDB/asgi.py
similarity index 73%
rename from FOSSDB_web/asgi.py
rename to src/FOSSDB/asgi.py
index e090765..11a6b23 100644
--- a/FOSSDB_web/asgi.py
+++ b/src/FOSSDB/asgi.py
@@ -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()
diff --git a/FOSSDB_web/settings.py b/src/FOSSDB/settings.py
similarity index 69%
rename from FOSSDB_web/settings.py
rename to src/FOSSDB/settings.py
index 34fb5ca..060c0c0 100644
--- a/FOSSDB_web/settings.py
+++ b/src/FOSSDB/settings.py
@@ -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
diff --git a/src/FOSSDB/urls.py b/src/FOSSDB/urls.py
new file mode 100644
index 0000000..7a48efe
--- /dev/null
+++ b/src/FOSSDB/urls.py
@@ -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)
diff --git a/FOSSDB_web/wsgi.py b/src/FOSSDB/wsgi.py
similarity index 73%
rename from FOSSDB_web/wsgi.py
rename to src/FOSSDB/wsgi.py
index 3d462f8..db8157c 100644
--- a/FOSSDB_web/wsgi.py
+++ b/src/FOSSDB/wsgi.py
@@ -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()
diff --git a/FOSSDB_web/apps/fossdb/__init__.py b/src/apps/account/__init__.py
similarity index 100%
rename from FOSSDB_web/apps/fossdb/__init__.py
rename to src/apps/account/__init__.py
diff --git a/src/apps/account/admin.py b/src/apps/account/admin.py
new file mode 100644
index 0000000..8d46c1b
--- /dev/null
+++ b/src/apps/account/admin.py
@@ -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)
diff --git a/src/apps/account/apps.py b/src/apps/account/apps.py
new file mode 100644
index 0000000..2c684a9
--- /dev/null
+++ b/src/apps/account/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AccountConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "account"
diff --git a/src/apps/account/forms.py b/src/apps/account/forms.py
new file mode 100644
index 0000000..c5ac173
--- /dev/null
+++ b/src/apps/account/forms.py
@@ -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",
+ )
diff --git a/src/apps/account/migrations/0001_initial.py b/src/apps/account/migrations/0001_initial.py
new file mode 100644
index 0000000..5b6cc36
--- /dev/null
+++ b/src/apps/account/migrations/0001_initial.py
@@ -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()),
+ ],
+ ),
+ ]
diff --git a/src/apps/account/migrations/0002_alter_user_profile_picture.py b/src/apps/account/migrations/0002_alter_user_profile_picture.py
new file mode 100644
index 0000000..f1e877e
--- /dev/null
+++ b/src/apps/account/migrations/0002_alter_user_profile_picture.py
@@ -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),
+ ),
+ ]
diff --git a/src/apps/account/migrations/0003_remove_user_profile_picture_profile.py b/src/apps/account/migrations/0003_remove_user_profile_picture_profile.py
new file mode 100644
index 0000000..0eb8801
--- /dev/null
+++ b/src/apps/account/migrations/0003_remove_user_profile_picture_profile.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/src/apps/account/migrations/0004_rename_profile_picture_profile_picture.py b/src/apps/account/migrations/0004_rename_profile_picture_profile_picture.py
new file mode 100644
index 0000000..f871c56
--- /dev/null
+++ b/src/apps/account/migrations/0004_rename_profile_picture_profile_picture.py
@@ -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',
+ ),
+ ]
diff --git a/FOSSDB_web/apps/fossdb/migrations/__init__.py b/src/apps/account/migrations/__init__.py
similarity index 100%
rename from FOSSDB_web/apps/fossdb/migrations/__init__.py
rename to src/apps/account/migrations/__init__.py
diff --git a/src/apps/account/models.py b/src/apps/account/models.py
new file mode 100644
index 0000000..a447cce
--- /dev/null
+++ b/src/apps/account/models.py
@@ -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()
diff --git a/src/apps/account/templates/login.html b/src/apps/account/templates/login.html
new file mode 100644
index 0000000..2552358
--- /dev/null
+++ b/src/apps/account/templates/login.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}
+
+{% endblock meta %}
+{% block content %}
+
+{% endblock %}
diff --git a/src/apps/account/templates/profile.html b/src/apps/account/templates/profile.html
new file mode 100644
index 0000000..3c93cfb
--- /dev/null
+++ b/src/apps/account/templates/profile.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}{% endblock %}
+{% block content %}
+ {{ user.username }}
+
+ {{ user.email }}
+{% endblock %}
diff --git a/src/apps/account/templates/signup.html b/src/apps/account/templates/signup.html
new file mode 100644
index 0000000..aa76e63
--- /dev/null
+++ b/src/apps/account/templates/signup.html
@@ -0,0 +1,45 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}
+
+{% endblock meta %}
+{% block content %}
+
+{% endblock %}
diff --git a/src/apps/account/tests.py b/src/apps/account/tests.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/account/urls.py b/src/apps/account/urls.py
new file mode 100644
index 0000000..c812fd2
--- /dev/null
+++ b/src/apps/account/urls.py
@@ -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("/", views.profile, name="profile"),
+]
diff --git a/src/apps/account/views.py b/src/apps/account/views.py
new file mode 100644
index 0000000..da3c102
--- /dev/null
+++ b/src/apps/account/views.py
@@ -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)
diff --git a/src/apps/fossdb/__init__.py b/src/apps/fossdb/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/fossdb/admin.py b/src/apps/fossdb/admin.py
new file mode 100644
index 0000000..89647d1
--- /dev/null
+++ b/src/apps/fossdb/admin.py
@@ -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)
diff --git a/src/apps/fossdb/apps.py b/src/apps/fossdb/apps.py
new file mode 100644
index 0000000..ba8e8cd
--- /dev/null
+++ b/src/apps/fossdb/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FossdbConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "fossdb"
diff --git a/src/apps/fossdb/filters.py b/src/apps/fossdb/filters.py
new file mode 100644
index 0000000..cc81ca6
--- /dev/null
+++ b/src/apps/fossdb/filters.py
@@ -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",
+ )
diff --git a/src/apps/fossdb/forms.py b/src/apps/fossdb/forms.py
new file mode 100644
index 0000000..ab183df
--- /dev/null
+++ b/src/apps/fossdb/forms.py
@@ -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(),
+ }
diff --git a/src/apps/fossdb/migrations/0001_initial.py b/src/apps/fossdb/migrations/0001_initial.py
new file mode 100644
index 0000000..b947c0a
--- /dev/null
+++ b/src/apps/fossdb/migrations/0001_initial.py
@@ -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'),
+ ),
+ ]
diff --git a/src/apps/fossdb/migrations/0002_alter_tag_icon.py b/src/apps/fossdb/migrations/0002_alter_tag_icon.py
new file mode 100644
index 0000000..5992142
--- /dev/null
+++ b/src/apps/fossdb/migrations/0002_alter_tag_icon.py
@@ -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/'),
+ ),
+ ]
diff --git a/src/apps/fossdb/migrations/0003_alter_projectprogramminglanguage_percentage.py b/src/apps/fossdb/migrations/0003_alter_projectprogramminglanguage_percentage.py
new file mode 100644
index 0000000..c5403b8
--- /dev/null
+++ b/src/apps/fossdb/migrations/0003_alter_projectprogramminglanguage_percentage.py
@@ -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),
+ ),
+ ]
diff --git a/src/apps/fossdb/migrations/0004_alter_tag_icon_star.py b/src/apps/fossdb/migrations/0004_alter_tag_icon_star.py
new file mode 100644
index 0000000..0ef0640
--- /dev/null
+++ b/src/apps/fossdb/migrations/0004_alter_tag_icon_star.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/src/apps/fossdb/migrations/0005_operatingsystem_is_linux.py b/src/apps/fossdb/migrations/0005_operatingsystem_is_linux.py
new file mode 100644
index 0000000..0448ee0
--- /dev/null
+++ b/src/apps/fossdb/migrations/0005_operatingsystem_is_linux.py
@@ -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),
+ ),
+ ]
diff --git a/src/apps/fossdb/migrations/__init__.py b/src/apps/fossdb/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/fossdb/models.py b/src/apps/fossdb/models.py
new file mode 100644
index 0000000..22697a1
--- /dev/null
+++ b/src/apps/fossdb/models.py
@@ -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)
diff --git a/src/apps/fossdb/templates/create_view.html b/src/apps/fossdb/templates/create_view.html
new file mode 100644
index 0000000..150fc0c
--- /dev/null
+++ b/src/apps/fossdb/templates/create_view.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}{% endblock %}
+{% block content %}
+
+
+{% endblock %}
diff --git a/src/apps/fossdb/templates/delete_view.html b/src/apps/fossdb/templates/delete_view.html
new file mode 100644
index 0000000..4b0f0d1
--- /dev/null
+++ b/src/apps/fossdb/templates/delete_view.html
@@ -0,0 +1,55 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}Delete {{ project }}{% endblock %}
+{% block content %}
+
+
+{% endblock %}
diff --git a/src/apps/fossdb/templates/detailed_view.html b/src/apps/fossdb/templates/detailed_view.html
new file mode 100644
index 0000000..84942a0
--- /dev/null
+++ b/src/apps/fossdb/templates/detailed_view.html
@@ -0,0 +1,123 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ project.owner }}/{{ project.name }}{% endblock %}
+{% block meta %}
+
+{% endblock meta %}
+{% block content %}
+
+
+
{{ project.name }}
+
+
+
+
+
+ {% for tag in project.tag.all|dictsort:"name" %}
+
+
+ {{ tag }}
+
+
+ {% endfor %}
+
+
+
+
+
{{ project.description }}
+
+
+
+
+
+
+
+
+
+
+ See project source code and read more here!
+
+
+
+
+
+
+
+
+ {% if user == project.owner %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/src/apps/fossdb/templates/explore.html b/src/apps/fossdb/templates/explore.html
new file mode 100644
index 0000000..bc46639
--- /dev/null
+++ b/src/apps/fossdb/templates/explore.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}{% endblock %}
+{% block content %}
+ {% for project in projects %}
+
+ {{ project.description }}
+
+ {% empty %}
+ No projects found
+ {% endfor %}
+{% endblock %}
diff --git a/src/apps/fossdb/tests.py b/src/apps/fossdb/tests.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/fossdb/urls.py b/src/apps/fossdb/urls.py
new file mode 100644
index 0000000..9359b2d
--- /dev/null
+++ b/src/apps/fossdb/urls.py
@@ -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("//", views.ProjectDetailView.as_view(), name="project-detail"),
+ path("//edit/", views.ProjectUpdateView.as_view(), name="project-update"),
+ path("//delete/", views.ProjectDeleteView.as_view(), name="project-delete"),
+]
diff --git a/src/apps/fossdb/views.py b/src/apps/fossdb/views.py
new file mode 100644
index 0000000..2e997ad
--- /dev/null
+++ b/src/apps/fossdb/views.py
@@ -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")
diff --git a/src/apps/main/__init__.py b/src/apps/main/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/main/admin.py b/src/apps/main/admin.py
new file mode 100644
index 0000000..e69de29
diff --git a/FOSSDB_web/apps/fossdb/apps.py b/src/apps/main/apps.py
similarity index 64%
rename from FOSSDB_web/apps/fossdb/apps.py
rename to src/apps/main/apps.py
index 6c8b002..167f044 100644
--- a/FOSSDB_web/apps/fossdb/apps.py
+++ b/src/apps/main/apps.py
@@ -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'
diff --git a/src/apps/main/migrations/__init__.py b/src/apps/main/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/main/models.py b/src/apps/main/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/main/templates/homepage.html b/src/apps/main/templates/homepage.html
new file mode 100644
index 0000000..c4b9433
--- /dev/null
+++ b/src/apps/main/templates/homepage.html
@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+{% load static %}
+{% block title %}{{ title }}{% endblock %}
+{% block meta %}{% endblock %}
+{% block content %}{% endblock %}
diff --git a/src/apps/main/tests.py b/src/apps/main/tests.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/main/urls.py b/src/apps/main/urls.py
new file mode 100644
index 0000000..2b187ec
--- /dev/null
+++ b/src/apps/main/urls.py
@@ -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"),
+]
diff --git a/src/apps/main/views.py b/src/apps/main/views.py
new file mode 100644
index 0000000..91d1a5d
--- /dev/null
+++ b/src/apps/main/views.py
@@ -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"})
diff --git a/src/apps/tokyonight_night/__init__.py b/src/apps/tokyonight_night/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/apps/tokyonight_night/apps.py b/src/apps/tokyonight_night/apps.py
new file mode 100644
index 0000000..217211d
--- /dev/null
+++ b/src/apps/tokyonight_night/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class Tokyonight_nightConfig(AppConfig):
+ name = 'tokyonight_night'
diff --git a/src/apps/tokyonight_night/static_src/.gitignore b/src/apps/tokyonight_night/static_src/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/src/apps/tokyonight_night/static_src/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/src/apps/tokyonight_night/static_src/package.json b/src/apps/tokyonight_night/static_src/package.json
new file mode 100644
index 0000000..2ca29f2
--- /dev/null
+++ b/src/apps/tokyonight_night/static_src/package.json
@@ -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"
+ }
+}
diff --git a/src/apps/tokyonight_night/static_src/postcss.config.js b/src/apps/tokyonight_night/static_src/postcss.config.js
new file mode 100644
index 0000000..0dcb6c9
--- /dev/null
+++ b/src/apps/tokyonight_night/static_src/postcss.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ plugins: {
+ "postcss-import": {},
+ "postcss-simple-vars": {},
+ "postcss-nested": {},
+ },
+}
diff --git a/src/apps/tokyonight_night/static_src/src/styles.css b/src/apps/tokyonight_night/static_src/src/styles.css
new file mode 100644
index 0000000..6d3a34f
--- /dev/null
+++ b/src/apps/tokyonight_night/static_src/src/styles.css
@@ -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%);
+}
diff --git a/src/apps/tokyonight_night/static_src/tailwind.config.js b/src/apps/tokyonight_night/static_src/tailwind.config.js
new file mode 100644
index 0000000..1f8e88b
--- /dev/null
+++ b/src/apps/tokyonight_night/static_src/tailwind.config.js
@@ -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 (/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//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"),
+ ],
+}
diff --git a/src/apps/tokyonight_night/templates/base.html b/src/apps/tokyonight_night/templates/base.html
new file mode 100644
index 0000000..6488240
--- /dev/null
+++ b/src/apps/tokyonight_night/templates/base.html
@@ -0,0 +1,94 @@
+{% load static tailwind_tags %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% tailwind_css %}
+
+ {% block meta %}{% endblock %}
+
+ {% block title %}{% endblock %}
+
+
+
+
+
+
+
+ {% if user.is_authenticated %}
+
+
{{ user.username }}
+
+
+
+
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
diff --git a/manage.py b/src/manage.py
similarity index 88%
rename from manage.py
rename to src/manage.py
index 94b8cd0..00cee50 100755
--- a/manage.py
+++ b/src/manage.py
@@ -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:
diff --git a/src/media/profile-pics/940e8e9e-77d4-42bb-84fd-04d332de403b.png b/src/media/profile-pics/940e8e9e-77d4-42bb-84fd-04d332de403b.png
new file mode 100644
index 0000000..a8c7eba
Binary files /dev/null and b/src/media/profile-pics/940e8e9e-77d4-42bb-84fd-04d332de403b.png differ
diff --git a/src/media/profile-pics/default.jpg b/src/media/profile-pics/default.jpg
new file mode 100644
index 0000000..82ff366
Binary files /dev/null and b/src/media/profile-pics/default.jpg differ
diff --git a/src/static/img/icons/logo.svg b/src/static/img/icons/logo.svg
new file mode 100644
index 0000000..9825f3c
--- /dev/null
+++ b/src/static/img/icons/logo.svg
@@ -0,0 +1,111 @@
+
+
+
+
diff --git a/src/static/js/buttons.js b/src/static/js/buttons.js
new file mode 100644
index 0000000..07bca2e
--- /dev/null
+++ b/src/static/js/buttons.js
@@ -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"
+ })
+})