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 -![Tests](https://github.com/kristoferssolo/FOSSDB-Web/actions/workflows/test.yml/badge.svg) -[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) +![Django Test](https://github.com/kristoferssolo/FOSSDB/actions/workflows/test.yml/badge.svg) +![Ruff](https://github.com/kristoferssolo/FOSSDB/actions/workflows/ruff.yml/badge.svg) FOSSDB is an open-source web application that helps users find, contribute, and collaborate on free and open-source software (FOSS) projects. @@ -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 %} +
+ {% csrf_token %} + + {% if form.username.errors %}

{{ form.username.errors }}

{% endif %} + + {% if form.password.errors %}

{{ form.password.errors }}

{% endif %} + +

+ Don't have an account? Create one Here! +

+
+{% 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.username }}’s profile picture +

{{ 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 %} +
+ {% csrf_token %} + + {% if form.username.errors %}

{{ form.username.errors }}

{% endif %} + + {% if form.email.errors %}

{{ form.email.errors }}

{% endif %} + + {% if form.password1.errors %}

{{ form.password1.errors }}

{% endif %} + + {% if form.password2.errors %}

{{ form.password2.errors }}

{% endif %} + +

+ Have an account? Login Here! +

+
+{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + {{ hosting_platform.management_form }} + {{ hosting_platform.as_table }} +
+ {{ programming_language.management_form }} + {% for form in programming_language %}
{{ form.as_table }}
{% endfor %} +
+ + + + + + +
+ +{% 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 %} +
+

To confirm, type "{{ project.owner.username }}/{{ project.name }}" in the box below

+ {% csrf_token %} + + +
+ +{% 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 }}

+

+ By {{ project.owner }} +

+ +
+ + + + + +
+ +
+ {% for tag in project.tag.all|dictsort:"name" %} + + + {{ tag }} + + + {% endfor %} +
+ +
+ {% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %} + + + {{ language.programming_language }} + + + {% 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 }} +
+
{{ 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 %} + + + +
+
+ +
+ logo + fossdb +
+ +
+ +
+ + +
+
+ + {% if user.is_authenticated %} +
+

{{ user.username }}

+ + {{ user.username }}'s profile picture + +
+ {% else %} + + {% endif %} +
+
+
+ {% block content %}{% endblock %} +
+
+
+
+

+ FOSSDB is a passion project of @kristoferssolo and a dedicated community of reporters. +

+ +

This site uses data from GitHub as well as data...

+

This site has no affiliation with GitHub or any other hosting platform.

+
+
+
+
+ + 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" + }) +})