Merge pull request #12 from kristoferssolo/development

Release v0.1.0
This commit is contained in:
Kristofers Solo 2023-06-29 19:54:38 +00:00 committed by GitHub
commit 27721e277d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 818 additions and 360 deletions

View File

@ -1,3 +0,0 @@
# from django.contrib import admin
# Register your models here.

View File

@ -1,3 +0,0 @@
# from django.db import models
# Create your models here.

View File

@ -1,3 +0,0 @@
# from django.test import TestCase
# Create your tests here.

View File

@ -1,3 +0,0 @@
# from django.shortcuts import render
# Create your views here.

View File

@ -2,5 +2,5 @@ Django==4.2.2
Pillow==9.5.0 Pillow==9.5.0
fontawesomefree==6.4.0 fontawesomefree==6.4.0
mysqlclient==2.1.1 mysqlclient==2.1.1
django-filter==23.2
django-tailwind==3.6.0 django-tailwind==3.6.0
django-search-views==0.3.7

View File

@ -1,34 +0,0 @@
[metadata]
name = FOSSDB-Web
description = Free and Open-Source Software Database Website
author = Kristofers Solo
license = GPL3
license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32
classifiers =
Programming Language :: Python :: 3.10
[options]
packages = FOSSDB_web
install_requires =
Django>=4.1
fontawesomefree>=6.2.1
mysqlclient>=2.1.1
python_requires = >=3.10
package_dir = =.
zip_safe = no
[options.extras_require]
testing =
flake8>=6.0.0
mypy>=0.991
pytest-cov>=4.0.0
pytest>=7.2.0
tox>=3.27.1
[options.package_data]
detector = py.typed
[flake8]
max-line-length = 160

View File

@ -38,10 +38,9 @@ INSTALLED_APPS = [
"main", "main",
"account", "account",
"fossdb", "fossdb",
"django_filters", "search_views",
"tailwind", "tailwind",
"tokyonight_night", "tokyonight_night",
"crispy_forms",
"fontawesomefree", "fontawesomefree",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
@ -51,6 +50,13 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
] ]
TAILWIND_APP_NAME = "tokyonight_night"
INTERNAL_IPS = [
"127.0.0.1",
]
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -157,12 +163,6 @@ LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/login/" LOGIN_URL = "/login/"
TAILWIND_APP_NAME = "tokyonight_night"
INTERNAL_IPS = [
"127.0.0.1",
]
# HTTPS settings # HTTPS settings
# SESSION_COOKIE_SECURE = True # SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True # CSRF_COOKIE_SECURE = True

View File

@ -19,10 +19,9 @@ from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", include("fossdb.urls")),
path("", include("main.urls")), path("", include("main.urls")),
path("", include("account.urls")), path("auth/", include("account.urls")),
path("", include("django.contrib.auth.urls")), path("<str:owner>/<str:project_name>/", include("fossdb.urls")),
] ]
if settings.DEBUG: if settings.DEBUG:
from django.conf.urls.static import static from django.conf.urls.static import static

View File

@ -1,12 +1,31 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserChangeForm as BaseUserChangeForm, UserCreationForm
from .models import User from .models import Profile, User
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.fields["username"].widget = forms.TextInput(
attrs={
"placeholder": "Username",
"class": "verify form-field submit-form",
}
)
self.fields["username"].label = ""
self.fields["password"].widget = forms.PasswordInput(
attrs={
"placeholder": "Password",
"class": "verify form-field submit-form",
}
)
self.fields["password"].label = ""
class SignUpForm(UserCreationForm): class SignUpForm(UserCreationForm):
email = forms.EmailField(required=False, help_text="Optional.") email = forms.EmailField(required=False)
class Meta: class Meta:
model = User model = User
@ -16,3 +35,99 @@ class SignUpForm(UserCreationForm):
"password1", "password1",
"password2", "password2",
) )
widgets = {
"username": forms.TextInput(
attrs={
"placeholder": "Username",
"class": "verify form-field submit-form",
}
),
"email": forms.EmailInput(
attrs={
"placeholder": "Email (optional)",
"class": "verify form-field submit-form",
}
),
"password1": forms.PasswordInput(
attrs={
"placeholder": "Password",
"class": "verify form-field submit-form",
}
),
"password2": forms.PasswordInput(
attrs={
"placeholder": "Confirm password",
"class": "verify form-field submit-form",
}
),
}
labels = {
"username": "",
"email": "",
"password1": "",
"password2": "",
}
class UserChangeForm(BaseUserChangeForm):
class Meta(BaseUserChangeForm.Meta):
model = User
fields = (
"username",
"email",
"first_name",
"last_name",
)
widgets = {
"username": forms.TextInput(
attrs={
"placeholder": "Username",
"class": "form-field submit-form",
}
),
"email": forms.EmailInput(
attrs={
"placeholder": "Email",
"class": "form-field submit-form",
}
),
"first_name": forms.TextInput(
attrs={
"placeholder": "First Name",
"class": "form-field submit-form",
}
),
"last_name": forms.TextInput(
attrs={
"placeholder": "Last Name",
"class": "form-field submit-form",
}
),
}
labels = {
"username": "",
"email": "",
"first_name": "",
"last_name": "",
}
help_text = {
"username": None,
"email": None,
"first_name": None,
"last_name": None,
}
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ("picture",)
labels = {
"picture": "",
}
help_text = {
"picture": None,
}

View File

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

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<div class="py-8 px-32">
<form method="POST"
enctype="multipart/form-data"
class="flex flex-col items-center justify-center space-y-4 my-auto">
<h2 class="text-3xl font-abel">Change password</h2>
{% csrf_token %}
{{ form }}
<button type="submit" class="button bg-skyblue-300 text-gray-500">Change</button>
</form>
</div>
{% endblock %}

View File

@ -3,8 +3,28 @@
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %} {% block meta %}{% endblock %}
{% block content %} {% block content %}
<h1>{{ user.username }}</h1> <div class="py-8 px-32">
<img src="{{ user.profile_picture.url }}" <div class="flex justify-end gap-4">
alt="{{ user.username }}s profile picture" /> <a href="{% url 'logout' %}">
<p>{{ user.email }}</p> <i class="fa-solid fa-right-from-bracket fa-2xl"></i>
</a>
<a href="{% url 'settings' %}">
<i class="fa-solid fa-gear fa-2xl"></i>
</a>
</div>
<h1 class="font-abel text-4xl mb-8">My Projects</h1>
<div class="grid grid-cols-1 gap-4">
{% for project in projects %}
<div class="border border-steelblue-100 p-8 flex flex-row gap-4 justify-between">
<a href="{{ project.get_absolute_url }}"
class="hover:text-steelblue-100 transition duration-300 ease-linear">
<h2 class="font-abel text-3xl">{{ project.name }}</h2>
</a>
<p class="max-w-2xl text-justify">{{ project.description|slice:500 }}</p>
</div>
{% empty %}
<p class="text-center font-abel text-xl">No projects yet.</p>
{% endfor %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<div class="py-8 px-32">
<div class="flex justify-end">
<a href="{% url 'change_password' %}">
<i class="fa-solid fa-shield fa-2xl"></i>
</a>
</div>
<form method="POST"
enctype="multipart/form-data"
class="flex flex-col items-center justify-center space-y-4 my-auto">
<h2 class="text-3xl font-abel">Edit profile</h2>
{% csrf_token %}
{{ user_form.username }}
{{ user_form.email }}
{{ user_form.first_name }}
{{ user_form.last_name }}
<button type="submit" class="button bg-skyblue-300 text-gray-500">Save</button>
</form>
</div>
{% endblock %}

View File

@ -13,33 +13,31 @@
placeholder="Username" placeholder="Username"
name="username" name="username"
value="{{ form.username.value|default:'' }}" value="{{ form.username.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" /> class="verify form-field submit-form" />
{% if form.username.errors %}<p class="text-indianred-100 text-xs italic">{{ form.username.errors }}</p>{% endif %} {% if form.username.errors %}<p class="text-indianred-100 text-xs italic">{{ form.username.errors }}</p>{% endif %}
<input type="email" <input type="email"
placeholder="Email (optional)" placeholder="Email (optional)"
name="email" name="email"
value="{{ form.email.value|default:'' }}" value="{{ form.email.value|default:'' }}"
class="mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" /> class="form-field submit-form" />
{% if form.email.errors %}<p class="text-indianred-100 text-xs italic">{{ form.email.errors }}</p>{% endif %} {% if form.email.errors %}<p class="text-indianred-100 text-xs italic">{{ form.email.errors }}</p>{% endif %}
<input type="password" <input type="password"
placeholder="Password" placeholder="Password"
name="password1" name="password1"
value="{{ form.password1.value|default:'' }}" value="{{ form.password1.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" /> class="verify form-field submit-form" />
{% if form.password1.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password1.errors }}</p>{% endif %} {% if form.password1.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password1.errors }}</p>{% endif %}
<input type="password" <input type="password"
placeholder="Confirm password" placeholder="Confirm password"
name="password2" name="password2"
value="{{ form.password2.value|default:'' }}" value="{{ form.password2.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" /> class="verify form-field submit-form" />
{% if form.password2.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password2.errors }}</p>{% endif %} {% if form.password2.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password2.errors }}</p>{% endif %}
<button type="submit" <button type="submit"
id="submit-button" id="submit-button"
class="px-12 py-2 border border-transparent rounded-md text-lightsteelblue-100 bg-slategray-200 opacity-50 transform ease-in-out duration-500 cursor-default"> class="button submit-button-disabled text-lightsteelblue-100 bg-slategray-200" />Sign Up</button>
Sign Up
</button>
<p> <p>
Have an account? Login <a class="underline text-skyblue-300" href="{% url 'login' %}">Here</a>! Have an account? Login <a class="underline text-skyblue-300" href="{% url 'login' %}">Here</a>!
</p> </p>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -5,5 +5,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path("signup/", views.signup_view, name="signup"), path("signup/", views.signup_view, name="signup"),
path("login/", views.login_view, name="login"), path("login/", views.login_view, name="login"),
path("<str:username>/", views.profile, name="profile"), path("logout/", views.LogoutView.as_view(), name="logout"),
path("settings/", views.ProfileUpdateView.as_view(), name="settings"),
path("settings/security/", views.PasswordChangeView.as_view(), name="change_password"),
] ]

View File

@ -1,19 +1,82 @@
from django.contrib.auth import authenticate, login from django.contrib import messages
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth import authenticate, login, logout, update_session_auth_hash
from django.shortcuts import get_object_or_404, redirect, render from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, render
from django.views.generic import ListView, TemplateView, View
from .forms import SignUpForm from fossdb.models import Project
from .models import User
from .forms import LoginForm, SignUpForm, UserChangeForm
def profile(request, username): class ProfileUpdateView(LoginRequiredMixin, TemplateView):
user = get_object_or_404(User, username=username) template_name = "setting.html"
login_url = "/login/"
redirect_field_name = "redirect_to"
def get(self, request):
user_form = UserChangeForm(instance=request.user)
context = {
"title": "Your profile",
"user_form": user_form,
}
return self.render_to_response(context)
def post(self, request):
user_form = UserChangeForm(request.POST, instance=request.user)
if user_form.is_valid():
user_form.save()
messages.add_message(request, messages.SUCCESS, "Your profile was successfully updated!")
context = { context = {
"title": user.username + ("" if not user.full_name else f" ({user.full_name})"), "title": "Your profile",
"user": user, "user_form": user_form,
} }
return render(request, "profile.html", context) return self.render_to_response(context)
class PasswordChangeView(LoginRequiredMixin, TemplateView):
template_name = "password.html"
def get(self, request):
form = PasswordChangeForm(user=request.user)
context = {
"title": "Change password",
"form": form,
}
return self.render_to_response(context)
def post(self, request):
form = PasswordChangeForm(data=request.POST, user=request.user)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.add_message(request, messages.SUCCESS, "Your password was successfully updated!")
context = {
"title": "Change password",
"form": form,
}
return self.render_to_response(context)
class ProfileProjectListView(LoginRequiredMixin, ListView):
model = Project
template_name = "profile.html"
context_object_name = "projects"
login_url = "/login/"
redirect_field_name = "redirect_to"
def get_queryset(self):
return Project.objects.filter(owner=self.request.user)
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs)
data["title"] = self.request.user.username + ("" if not self.request.user.full_name else f" ({self.request.user.full_name})")
return data
def signup_view(request): def signup_view(request):
@ -34,7 +97,7 @@ def signup_view(request):
def login_view(request): def login_view(request):
form = AuthenticationForm(data=request.POST or None) form = LoginForm(data=request.POST or None)
if request.method == "POST": if request.method == "POST":
if form.is_valid(): if form.is_valid():
user = form.get_user() user = form.get_user()
@ -46,3 +109,9 @@ def login_view(request):
"form": form, "form": form,
} }
return render(request, "login.html", context) return render(request, "login.html", context)
class LogoutView(View):
def get(self, request):
logout(request)
return redirect("login")

View File

@ -1,28 +0,0 @@
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",
)

View File

@ -1,19 +1,33 @@
from django import forms from django import forms
from django.forms import inlineformset_factory
from .models import HostingPlatform, ProgrammingLanguage, Project, ProjectHostingPlatform, ProjectProgrammingLanguage from .models import HostingPlatform, License, OperatingSystemVersion, ProgrammingLanguage, Project, ProjectHostingPlatform, ProjectProgrammingLanguage, Tag
class HostingPlatformForm(forms.ModelForm): class HostingPlatformForm(forms.ModelForm):
class Meta: class Meta:
model = ProjectHostingPlatform model = ProjectHostingPlatform
fields = ( fields = (
"url",
"hosting_platform", "hosting_platform",
"url",
) )
widgets = { widgets = {
"hosting_platform": forms.Select( "hosting_platform": forms.Select(
choices=HostingPlatform.objects.all(), choices=HostingPlatform.objects.all(),
) attrs={
"class": "form-field submit-form",
},
),
"url": forms.URLInput(
attrs={
"placeholder": "url",
"class": "form-field submit-form font-roboto",
},
),
}
labels = {
"hosting_platform": "",
"url": "",
} }
@ -27,14 +41,32 @@ class ProgrammingLanguageForm(forms.ModelForm):
widgets = { widgets = {
"programming_language": forms.Select( "programming_language": forms.Select(
choices=ProgrammingLanguage.objects.all(), choices=ProgrammingLanguage.objects.all(),
attrs={
"class": "form-field submit-form",
},
), ),
"percentage": forms.NumberInput( "percentage": forms.NumberInput(
attrs={ attrs={
"placeholder": "Percentage",
"class": "form-field submit-form",
"min": "0", "min": "0",
"max": "100", "max": "100",
} },
), ),
} }
labels = {
"programming_language": "",
"percentage": "",
}
ProgrammingLanguageInlineFormSet = inlineformset_factory(
Project,
ProjectProgrammingLanguage,
form=ProgrammingLanguageForm,
extra=0,
can_delete=True,
)
class ProjectForm(forms.ModelForm): class ProjectForm(forms.ModelForm):
@ -51,17 +83,39 @@ class ProjectForm(forms.ModelForm):
widgets = { widgets = {
"name": forms.TextInput( "name": forms.TextInput(
attrs={ attrs={
"class": "form-control",
"placeholder": "Project name", "placeholder": "Project name",
} "class": "form-field submit-form font-roboto",
},
), ),
"description": forms.Textarea( "description": forms.Textarea(
attrs={ attrs={
"class": "form-control",
"placeholder": "Description", "placeholder": "Description",
} "class": "form-field submit-form font-roboto",
},
),
"license": forms.CheckboxSelectMultiple(
choices=License.objects.all(),
attrs={
"class": "checkbox-form",
},
),
"operating_system": forms.CheckboxSelectMultiple(
choices=OperatingSystemVersion.objects.all(),
attrs={
"class": "checkbox-form",
},
),
"tag": forms.CheckboxSelectMultiple(
choices=Tag.objects.all(),
attrs={
"class": "checkbox-form",
},
), ),
"license": forms.CheckboxSelectMultiple(), }
"tag": forms.CheckboxSelectMultiple(), labels = {
"operating_system": forms.CheckboxSelectMultiple(), "name": "",
"description": "",
"license": "",
"tag": "",
"operating_system": "",
} }

View File

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

View File

@ -0,0 +1,43 @@
# Generated by Django 4.2.2 on 2023-06-29 15:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0006_alter_projectprogramminglanguage_percentage'),
]
operations = [
migrations.AlterField(
model_name='license',
name='full_name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='license',
name='short_name',
field=models.CharField(db_index=True, max_length=50, unique=True),
),
migrations.AlterField(
model_name='operatingsystem',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='programminglanguage',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='project',
name='description',
field=models.TextField(blank=True, db_index=True, default=''),
),
migrations.AlterField(
model_name='project',
name='name',
field=models.CharField(db_index=True, max_length=255),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 15:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0007_alter_license_full_name_alter_license_short_name_and_more'),
]
operations = [
migrations.AlterField(
model_name='project',
name='description',
field=models.TextField(blank=True, default=''),
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 4.2.2 on 2023-06-29 16:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0008_alter_project_description'),
]
operations = [
migrations.DeleteModel(
name='Star',
),
]

View File

@ -6,8 +6,8 @@ from django.db import models
class License(models.Model): class License(models.Model):
short_name = models.CharField(max_length=50, unique=True) short_name = models.CharField(max_length=50, unique=True, db_index=True)
full_name = models.CharField(max_length=100, unique=True) full_name = models.CharField(max_length=100, unique=True, db_index=True)
url = models.URLField(blank=True, default="") url = models.URLField(blank=True, default="")
text = models.TextField(blank=True, default="") text = models.TextField(blank=True, default="")
@ -16,7 +16,7 @@ class License(models.Model):
class OperatingSystem(models.Model): class OperatingSystem(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True, db_index=True)
description = models.TextField(blank=True, default="") description = models.TextField(blank=True, default="")
is_linux = models.BooleanField(blank=True, default=False) is_linux = models.BooleanField(blank=True, default=False)
@ -55,7 +55,7 @@ class Tag(models.Model):
class ProgrammingLanguage(models.Model): class ProgrammingLanguage(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True, db_index=True)
def __str__(self): def __str__(self):
return self.name return self.name
@ -64,7 +64,7 @@ class ProgrammingLanguage(models.Model):
class Project(models.Model): class Project(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name="ID") 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) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255, db_index=True)
description = models.TextField(blank=True, default="") description = models.TextField(blank=True, default="")
license = models.ManyToManyField(License, blank=True) license = models.ManyToManyField(License, blank=True)
tag = models.ManyToManyField(Tag, blank=True) tag = models.ManyToManyField(Tag, blank=True)
@ -72,10 +72,6 @@ class Project(models.Model):
programming_language = models.ManyToManyField(ProgrammingLanguage, through="ProjectProgrammingLanguage", blank=True) programming_language = models.ManyToManyField(ProgrammingLanguage, through="ProjectProgrammingLanguage", blank=True)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@property
def star_amount(self):
return self.star.count()
@property @property
def runs_on_macos(self): def runs_on_macos(self):
return self.operating_system.filter(operating_system__name="macOS").exists() return self.operating_system.filter(operating_system__name="macOS").exists()
@ -117,7 +113,7 @@ class Project(models.Model):
class ProjectProgrammingLanguage(models.Model): class ProjectProgrammingLanguage(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE)
programming_language = models.ForeignKey(ProgrammingLanguage, on_delete=models.CASCADE) programming_language = models.ForeignKey(ProgrammingLanguage, on_delete=models.CASCADE)
percentage = models.PositiveIntegerField(blank=True, null=True) percentage = models.PositiveIntegerField(blank=True, default=0)
def __str__(self): def __str__(self):
return f"{self.project.owner}/{self.project.name} | {self.programming_language} | {self.percentage}%" return f"{self.project.owner}/{self.project.name} | {self.programming_language} | {self.percentage}%"
@ -137,8 +133,3 @@ class ProjectHostingPlatform(models.Model):
def __str__(self): def __str__(self):
return self.url return self.url
class Star(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)

View File

@ -3,32 +3,62 @@
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %} {% block meta %}{% endblock %}
{% block content %} {% block content %}
<form method="post" id="project-form"> <form class="flex flex-col items-center justify-center space-y-4 my-auto font-condensed"
method="post"
id="project-form">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.name }}
{{ hosting_platform.management_form }} {{ form.description }}
{{ hosting_platform.as_table }} <div class="flex gap-4 justify-center">
<div id="language-formset"> <div class="">
{{ programming_language.management_form }} <p class="text-xl font-abel text-center">Licenses</p>
{% for form in programming_language %}<div class="language-form">{{ form.as_table }}</div>{% endfor %} {{ form.license }}
</div> </div>
<!-- This button will trigger the JS to append another language form --> <div>
<button type="button" id="add-more">+</button> <p class="text-xl font-abel text-center">Tags</p>
<!-- Render the empty form, which you'll use as a template for new entries --> {{ form.tag }}
<!-- Wrap it in a container so you can reference it by id and hide it --> </div>
<div id="empty-form" style="display:none;">{{ empty_form.as_table }}</div> <div>
<button type="submit">Submit</button> <p class="text-xl font-abel text-center">Operating systems</p>
{{ form.operating_system }}
</div>
</div>
{{ hosting_platform.management_form }}
<div class="flex gap-4 items-center">
<p class="text-xl font-abel">Hosting platform:</p>
{{ hosting_platform }}
</div>
{{ programming_languages.management_form }}
<!-- Languages container -->
<div class="" id="languages-container">
{% for language in programming_languages %}<div class="flex gap-4 items-center">{{ language }}</div>{% endfor %}
</div>
<!-- Hidden empty form used as a template -->
<div id="empty-form" style="display:none">
<div class="flex gap-4 items-center">{{ programming_languages.empty_form }}</div>
</div>
<div class="flex justify-between">
<div class="p-2 text-mediumaquamarine cursor-pointer hover:opacity-50 transition duration-100 ease-linear"
id="add-language">
<i class="fa-solid fa-plus fa-2xl"></i>
</div>
</div>
<button class="button submit-button-enabled bg-mediumaquamarine text-gray-500"
ype="submit">Save</button>
</form> </form>
<script> <script type="text/javascript">
document.querySelector("#add-more").addEventListener("click", function() { window.addEventListener("DOMContentLoaded", () => {
var formIndex = document.querySelector("#id_language-TOTAL_FORMS").value; document.getElementById("add-language").addEventListener("click", () => {
var emptyFormDiv = document.querySelector("#empty-form"); var formIndex = document.getElementById("id_projectprogramminglanguage_set-TOTAL_FORMS");
var newFormHTML = emptyFormDiv.innerHTML.replace(/__prefix__/g, formIndex); var emptyForm = document.getElementById("empty-form").innerHTML;
var newFormDiv = document.createElement("div");
newFormDiv.className = "language-form"; emptyForm = emptyForm.replace(/__prefix__/g, formIndex.value);
newFormDiv.innerHTML = newFormHTML;
document.querySelector("#language-formset").append(newFormDiv); document.getElementById("languages-container")
document.querySelector("#id_language-TOTAL_FORMS").value = parseInt(formIndex) + 1; .insertAdjacentHTML("beforeend", emptyForm);
});
formIndex.value = parseInt(formIndex.value) + 1;
})
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block title %}Delete {{ project }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content %} {% block content %}
<form method="post" <form method="post"
id="delete-form" id="delete-form"
@ -9,9 +9,9 @@
{% csrf_token %} {% csrf_token %}
<input type="text" <input type="text"
id="confirm-input" id="confirm-input"
class="mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300 text-center" /> class="text-center form-field border-slategray-200 hover:border-lightcoral bg-gray-300 focus:border-indianred-100 transition ease-linear" />
<button id="submit-button" <button id="submit-button"
class="px-12 py-2 border border-transparent rounded-md text-lightsteelblue-100 font-bold uppercase bg-slategray-200 opacity-50 transform ease-in-out duration-500 cursor-default" class="button submit-button-disabled text-lightsteelblue-100 bg-slategray-200 uppercase font-bold"
type="submit">Delete</button> type="submit">Delete</button>
</form> </form>
<script type="text/javascript"> <script type="text/javascript">
@ -23,30 +23,26 @@
USER_INPUT.addEventListener("input", () => { USER_INPUT.addEventListener("input", () => {
if (confirm_string == USER_INPUT.value) { if (confirm_string == USER_INPUT.value) {
SUBMIT_BUTTON.classList.remove( SUBMIT_BUTTON.classList.remove(
"bg-slategray-200", "submit-button-disabled",
"text-lightsteelblue-100", "text-lightsteelblue-100",
"opacity-50", "bg-slategray-200"
"cursor-default"
) )
SUBMIT_BUTTON.classList.add( SUBMIT_BUTTON.classList.add(
"bg-indianred-100", "submit-button-enabled",
"text-gray-500", "text-gray-500",
"opacity-100", "bg-indianred-100",
"hover:opacity-60"
) )
SUBMIT_BUTTON.disabled = false SUBMIT_BUTTON.disabled = false
} else { } else {
SUBMIT_BUTTON.classList.remove( SUBMIT_BUTTON.classList.remove(
"bg-indianred-100", "submit-button-enabled",
"text-gray-500", "text-gray-500",
"opacity-100", "bg-indianred-100",
"hover:opacity-60"
) )
SUBMIT_BUTTON.classList.add( SUBMIT_BUTTON.classList.add(
"bg-slategray-200", "submit-button-disabled",
"text-lightsteelblue-100", "text-lightsteelblue-100",
"opacity-50", "bg-slategray-200"
"cursor-default"
) )
SUBMIT_BUTTON.disabled = true SUBMIT_BUTTON.disabled = true
} }

View File

@ -13,45 +13,73 @@
</h2> </h2>
<!-- os platform icons --> <!-- os platform icons -->
<div class="my-4 flex justify-center items-center gap-x-4"> <div class="my-4 flex justify-center items-center gap-x-4">
<a href=""><i class="fa-brands fa-linux fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <!-- linux -->
<a href="/search/?q=linux">
<span title="Linux"
class="hover:text-lightsteelblue-200 transform duration-200 ease-linear
{% if project.runs_on_linux %} {% if project.runs_on_linux %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}">
<a href=""><i class="fa-brands fa-windows fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <i class="fa-brands fa-linux fa-lg"></i>
</span>
</a>
<!-- windows -->
<a href="/search/?q=windows">
<span title="Windows"
class="hover:text-lightsteelblue-200 transform duration-200 ease-linear
{% if project.runs_on_windows %} {% if project.runs_on_windows %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}">
<a href=""><i class="fa-brands fa-apple fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <i class="fa-brands fa-windows fa-lg"></i>
</span>
</a>
<!-- macos -->
<a href="/search/?q=macos">
<span title="macOS"
class="hover:text-lightsteelblue-200 transform duration-200 ease-linear
{% if project.runs_on_macos %} {% if project.runs_on_macos %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}">
<a href=""><i id="ios" <i class="fa-brands fa-apple fa-lg"></i>
class="fa-brands fa-app-store-ios fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out </span>
</a>
<!-- ios -->
<a href="/search/?q=ios">
<span title="iOS"
class="fa-lg hover:text-lightsteelblue-200 transform duration-200 ease-linear
{% if project.runs_on_ios %} {% if project.runs_on_ios %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}">
<a href=""><i id="android" <i class="fa-brands fa-app-store-ios"></i>
class="fa-brands fa-android fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out </span>
</a>
<!-- android -->
<a href="/search/?q=android">
<span title="Android"
class="hover:text-lightsteelblue-200 transform duration-200 ease-linear
{% if project.runs_on_android %} {% if project.runs_on_android %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}">
<i class="fa-brands fa-android fa-lg"></i>
</span>
</a>
</div> </div>
<!-- tags --> <!-- tags -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2"> <div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for tag in project.tag.all|dictsort:"name" %} {% for tag in project.tag.all|dictsort:"name" %}
<a href=""> <a href="/search/?q={{ tag.name }}">
<span title="{{ tag.description }}" <span title="{{ tag.description }}"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out"> class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-200 ease-linear">
{{ tag }} {{ tag }}
</span> </span>
</a> </a>
@ -60,9 +88,9 @@
<!-- programming languages --> <!-- programming languages -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2"> <div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %} {% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %}
<a href=""> <a href="/search/?q={{ language.name }}">
<span title="{{ language.percentage }}%" <span title="{{ language.percentage }}%"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out"> class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-200 ease-linear">
{{ language.programming_language }} {{ language.programming_language }}
</span> </span>
</a> </a>
@ -75,7 +103,7 @@
<div class="flex justify-between w-full mt-4 font-abel"> <div class="flex justify-between w-full mt-4 font-abel">
<!-- licenses --> <!-- licenses -->
<div> <div>
<button class="text-xl underline font-abel hover:text-lightsteelblue-200 transform duration-300 ease-in-out" <button class="text-xl underline font-abel hover:text-lightsteelblue-200 transform duration-200 ease-linear"
id="menu-button" id="menu-button"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true">Licenses</button> aria-expanded="true">Licenses</button>
@ -83,7 +111,7 @@
<!-- hosting platform --> <!-- hosting platform -->
<div class=""> <div class="">
<p> <p>
See project source code and read more <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-in-out" See project source code and read more <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-200 ease-linear"
href="{{ project.projecthostingplatform.url }}" href="{{ project.projecthostingplatform.url }}"
target="_blank">here</a>! target="_blank">here</a>!
</p> </p>
@ -100,7 +128,7 @@
{% for license in project.license.all|dictsort:"short_name" %} {% for license in project.license.all|dictsort:"short_name" %}
<a href="{{ license.url }}"> <a href="{{ license.url }}">
<span title="{{ license.full_name }}" <span title="{{ license.full_name }}"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60"> class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-200 ease-linear">
{{ license }} {{ license }}
</span> </span>
</a> </a>
@ -111,12 +139,12 @@
</div> </div>
{% if user == project.owner %} {% if user == project.owner %}
<div class="flex justify-between mt-8 mx-16"> <div class="flex justify-between mt-8 mx-16">
<button class="px-12 py-2 border border-transparent rounded-md text-gray-500 bg-skyblue-300 hover:opacity-60 transform ease-in-out duration-500"> <a href="{% url 'project-update' project.owner project.name %}">
<a href="{% url 'project-update' project.owner project.name %}">Update</a> <button class="button hover:bg-opacity-60 bg-mediumpurple-100 text-gray-500">Update</button>
</button> </a>
<button class="px-12 py-2 border border-transparent rounded-md text-gray-500 bg-burlywood hover:opacity-60 transform ease-in-out duration-500"> <a href="{% url 'project-delete' project.owner project.name %}">
<a href="{% url 'project-delete' project.owner project.name %}">Delete</a> <button class="button hover:bg-opacity-60 bg-lightcoral text-gray-500">Delete</button>
</button> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -3,13 +3,20 @@
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %} {% block meta %}{% endblock %}
{% block content %} {% block content %}
<div class="py-8 px-16">
<h1 class="font-abel text-4xl mb-8">Projects</h1>
<div class="grid grid-cols-2 gap-4">
{% for project in projects %} {% for project in projects %}
<div> <div class="border border-steelblue-100 p-8 flex flex-row gap-4 justify-between">
<a href="{{ project.get_absolute_url }}">{{ project }}</a> <a href="{{ project.get_absolute_url }}"
class="hover:text-steelblue-100 transition duration-300 ease-linear">
<h2 class="font-abel text-2xl">{{ project.name }}</h2>
</a>
<p class="max-w-xs text-justify">{{ project.description|slice:500 }}</p>
</div> </div>
<div>{{ project.description }}</div>
<hr />
{% empty %} {% empty %}
<p>No projects found</p> <p class="text-center font-abel text-xl">No projects yet.</p>
{% endfor %} {% endfor %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<div class="py-8 px-32">
<h1 class="font-abel text-4xl mb-8">Search Results</h1>
<div class="grid grid-cols-1 gap-4">
{% for project in projects %}
<div class="border border-steelblue-100 p-8 flex flex-row gap-4 justify-between">
<a href="{{ project.get_absolute_url }}"
class="hover:text-steelblue-100 transition duration-300 ease-linear">
<h2 class="font-abel text-3xl">{{ project.name }}</h2>
</a>
<p class="max-w-2xl text-justify">{{ project.description|slice:500 }}</p>
</div>
{% empty %}
<p class="text-center font-abel text-xl">No projects yet.</p>
{% endfor %}
</div>
</div>
{% endblock %}

View File

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

View File

@ -1,64 +1,84 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.forms import inlineformset_factory from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView from django.urls import reverse_lazy
from django_filters.views import FilterView from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from .filters import ProjectFilter from .forms import HostingPlatformForm, ProgrammingLanguageInlineFormSet, ProjectForm
from .models import Project
from .forms import HostingPlatformForm, ProgrammingLanguageForm, ProjectForm
from .models import Project, ProjectProgrammingLanguage
ProgrammingLanguageInlineFormset = inlineformset_factory(
Project,
ProjectProgrammingLanguage,
form=ProgrammingLanguageForm,
extra=1,
)
class ProjectListView(FilterView): class SearchResultsListView(ListView):
model = Project
template_name = "search.html"
context_object_name = "projects"
def get_queryset(self):
query = self.request.GET.get("q")
return Project.objects.filter(
Q(owner__username__icontains=query)
| Q(name__icontains=query)
# | Q(description__icontains=query)
| Q(license__short_name__icontains=query)
| Q(license__full_name__icontains=query)
| Q(tag__name__icontains=query)
| Q(operating_system__operating_system__name__icontains=query)
| Q(operating_system__codename__icontains=query)
| Q(programming_language__name__icontains=query)
).distinct()
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs)
data["title"] = "FOSSDB | Search"
return data
class ProjectListView(ListView):
model = Project model = Project
template_name = "explore.html" template_name = "explore.html"
filterset_class = ProjectFilter
context_object_name = "projects" context_object_name = "projects"
paginate_by = 100 # optional 10 projects a page paginate_by = 50 # amount of items on screen
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs)
data["title"] = "FOSSDB | Explore"
return data
class ProjectCreateView(LoginRequiredMixin, CreateView): class ProjectCreateView(LoginRequiredMixin, CreateView):
model = Project model = Project
form_class = ProjectForm form_class = ProjectForm
template_name = "create_view.html" template_name = "create_view.html"
login_url = "/login/" login_url = reverse_lazy("login")
redirect_field_name = "redirect_to" redirect_field_name = "redirect_to"
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["hosting_platform"] = HostingPlatformForm(self.request.POST or None, prefix="hosting") data["title"] = "FOSSDB | Create Project"
data["programming_language"] = ProgrammingLanguageInlineFormset(self.request.POST or None, prefix="language") data["hosting_platform"] = HostingPlatformForm(self.request.POST or None)
data["empty_form"] = ProgrammingLanguageInlineFormset(prefix="language_empty") data["programming_languages"] = ProgrammingLanguageInlineFormSet(self.request.POST or None)
return data return data
def form_valid(self, form): def form_valid(self, form):
context = self.get_context_data() context = self.get_context_data()
form.instance.owner = self.request.user form.instance.owner = self.request.user
self.object = form.save() # Save project form
hosting_platform = context["hosting_platform"] hosting_platform = context["hosting_platform"]
programming_language = context["programming_language"]
self.object = form.save()
if hosting_platform.is_valid(): if hosting_platform.is_valid():
hosting_platform.instance.project = self.object hosting_platform = hosting_platform.save(commit=False)
hosting_platform.project = self.object
hosting_platform.save() hosting_platform.save()
# TODO: allow adding multiple languages
if programming_language.is_valid(): programming_languages = context["programming_languages"]
for instance in programming_language.save(commit=False): if programming_languages.is_valid():
instance.project = self.object programming_languages.instance = self.object
instance.save() programming_languages.save()
programming_language.save_m2m()
if hosting_platform.is_valid() and programming_language.is_valid():
return super().form_valid(form) return super().form_valid(form)
else:
return self.render_to_response(self.get_context_data(form=form))
class ProjectDetailView(DetailView): class ProjectDetailView(DetailView):
@ -75,7 +95,7 @@ class ProjectUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
form_class = ProjectForm form_class = ProjectForm
slug_field = "name" slug_field = "name"
slug_url_kwarg = "project_name" slug_url_kwarg = "project_name"
login_url = "/login/" login_url = reverse_lazy("login")
redirect_field_name = "redirect_to" redirect_field_name = "redirect_to"
def test_func(self): def test_func(self):
@ -85,24 +105,48 @@ class ProjectUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
return redirect("login") return redirect("login")
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super(ProjectUpdateView, self).get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["hosting_platform"] = HostingPlatformForm(self.request.POST or None, instance=self.object.projecthostingplatform, prefix="hosting") data["title"] = f"Edit {self.object}"
data["programming_language"] = ProgrammingLanguageInlineFormset(self.request.POST or None, instance=self.object, prefix="language") data["hosting_platform"] = HostingPlatformForm(self.request.POST or None, instance=self.object.projecthostingplatform)
data["empty_form"] = ProgrammingLanguageInlineFormset(prefix="language_empty") data["programming_languages"] = ProgrammingLanguageInlineFormSet(self.request.POST or None, instance=self.object)
return data return data
def form_valid(self, form):
context = self.get_context_data()
form.instance.owner = self.request.user
self.object = form.save() # Save project form
hosting_platform = context["hosting_platform"]
if hosting_platform.is_valid():
hosting_platform.project = self.object
hosting_platform.save()
programming_languages = context["programming_languages"]
if programming_languages.is_valid():
programming_languages.instance = self.object
programming_languages.save()
return super().form_valid(form)
class ProjectDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): class ProjectDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Project model = Project
template_name = "delete_view.html" template_name = "delete_view.html"
slug_field = "name" slug_field = "name"
slug_url_kwarg = "project_name" slug_url_kwarg = "project_name"
login_url = "/login/" login_url = reverse_lazy("login")
redirect_field_name = "redirect_to" redirect_field_name = "redirect_to"
success_url = "/" success_url = reverse_lazy("explore")
def test_func(self): def test_func(self):
return self.get_object().owner == self.request.user return self.get_object().owner == self.request.user
def handle_no_permission(self): def handle_no_permission(self):
return redirect("login") return redirect("login")
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs)
data["title"] = f"Delete {self.object}"
return data

View File

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

View File

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

View File

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

View File

@ -1,11 +1,20 @@
from account.views import ProfileProjectListView
from django.urls import path from django.urls import path
from fossdb.views import ProjectCreateView, ProjectListView, SearchResultsListView
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.homepage, name="homepage"), path("", views.homepage, name="homepage"),
path("contribute/", views.contribute, name="contribute"), path("search/", SearchResultsListView.as_view(), name="search"),
path("news/", views.news, name="news"), path("explore/", ProjectListView.as_view(), name="explore"),
path("contribute/", ProjectCreateView.as_view(), name="contribute"),
path("dashboard/", views.dashboard, name="dashboard"), path("dashboard/", views.dashboard, name="dashboard"),
path("news/", views.news, name="news"),
path("help/", views.help, name="help"), path("help/", views.help, name="help"),
path("login/", views.login),
path("logout/", views.logout),
path("signup/", views.signup),
path("<str:username>/", ProfileProjectListView.as_view(), name="profile"),
] ]

View File

@ -1,10 +1,22 @@
from django.shortcuts import render from django.shortcuts import redirect, render
def homepage(request): def homepage(request):
return render(request, "homepage.html", {"title": "FOSSDB"}) return render(request, "homepage.html", {"title": "FOSSDB"})
def login(request):
return redirect("login")
def logout(request):
return redirect("logout")
def signup(request):
return redirect("signup")
def contribute(request): def contribute(request):
return render(request, "contribute.html", {"title": "FOSSDB | Contribute"}) return render(request, "contribute.html", {"title": "FOSSDB | Contribute"})

View File

@ -2,10 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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 { body {
@apply leading-[normal] m-0; @apply leading-[normal] m-0;
} }
@ -28,3 +24,27 @@ body {
background: linear-gradient(to right, transparent, #27a1b9, transparent); background: linear-gradient(to right, transparent, #27a1b9, transparent);
transform: translateY(-50%); transform: translateY(-50%);
} }
.form-field {
@apply mt-1 block rounded-md border-2;
}
.submit-form {
@apply border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-slategray-100 focus:border-cadetblue-300;
}
.checkbox-form {
@apply border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-slategray-100 focus:border-cadetblue-300;
}
.button {
@apply font-condensed text-xl px-12 py-2 border rounded-md transform ease-linear duration-100;
}
.submit-button-disabled {
@apply opacity-60 cursor-default;
}
.submit-button-enabled {
@apply opacity-100 hover:opacity-60 hover:bg-opacity-60;
}

View File

@ -7,11 +7,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Abel&family=Rationale&family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"
rel="stylesheet" /> rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Abel&family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" <link href="https://fonts.googleapis.com/css2?family=Abel&display=swap"
rel="stylesheet" /> rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" <link href="https://fonts.googleapis.com/css2?family=Rationale&display=swap"
rel="stylesheet" /> rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" <link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" /> rel="stylesheet" />
@ -27,32 +27,35 @@
<header class="flex justify-between items-center px-6 py-2 font-abel text-xl"> <header class="flex justify-between items-center px-6 py-2 font-abel text-xl">
<div class="flex justify-between"> <div class="flex justify-between">
<!-- logo --> <!-- logo -->
<div class="flex items-center text-4xl font-abel"> <div class="flex items-center text-4xl font-rationale">
<img class="" <img class=""
width="40" width="40"
src="{% static 'img/icons/logo.svg' %}" src="{% static 'img/icons/logo.svg' %}"
alt="logo" /> alt="logo" />
<a class="hover:text-skyblue-300 transform duration-300 ease-in-out" <a class="hover:text-skyblue-300 transform duration-300 ease-linear"
href="{% url 'homepage' %}">foss<span class="text-skyblue-300">db</span></a> href="{% url 'homepage' %}">foss<span class="text-skyblue-300">db</span></a>
</div> </div>
<!-- search --> <!-- search -->
<div class="relative items-center flex"> <form action="{% url 'search' %}"
method="get"
class="relative items-center flex">
<input type="text" <input type="text"
name="q"
placeholder="Search projects..." placeholder="Search projects..."
class="py-2 mx-4 placeholder-slategray-100 bg-gray-300 border-0 border-b-2 border-b-slategray-100 hover:border-b-lightsteelblue-100 transform duration-200 ease-in-out focus:outline-none focus:border-b-skyblue-300 focus:border-b-2 max-w-[10rem]" /> class="py-2 mx-4 placeholder-slategray-100 bg-gray-300 border-0 border-b-2 border-b-slategray-100 hover:border-b-lightsteelblue-100 transform duration-200 ease-linear focus:outline-none focus:border-b-skyblue-300 focus:border-b-2 max-w-[10rem]" />
</div> </form>
<!-- navbar --> <!-- navbar -->
<nav class="uppercase flex gap-x-6 items-center"> <nav class="uppercase flex gap-x-6 items-center">
<a href="{% url 'explore' %}" <a href="{% url 'explore' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">explore</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">explore</a>
<a href="{% url 'contribute' %}" <a href="{% url 'contribute' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">contribute</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">contribute</a>
<a href="{% url 'news' %}" <a href="{% url 'news' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">news</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">news</a>
<a href="{% url 'dashboard' %}" <a href="{% url 'dashboard' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">dashboard</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">dashboard</a>
<a href="{% url 'help' %}" <a href="{% url 'help' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">help</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">help</a>
</nav> </nav>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -61,14 +64,12 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="mx-4">{{ user.username }}</p> <p class="mx-4">{{ user.username }}</p>
<a href="{% url 'profile' user.username %}"> <a href="{% url 'profile' user.username %}">
<img src="{{ user.profile.picture.url }}" <img src="{{ user.profile.picture.url }}" class="w-[2rem]" alt="pic" />
class="w-[2rem]"
alt="{{ user.username }}'s profile picture" />
</a> </a>
</div> </div>
{% else %} {% else %}
<a href="{% url 'login' %}" <a href="{% url 'login' %}"
class="hover:text-skyblue-300 transition duration-300 ease-in-out"><i class="fa-solid fa-user fa-lg ml-4 "></i></a> class="hover:text-skyblue-300 transition duration-300 ease-linear"><i class="fa-solid fa-user fa-lg ml-4 "></i></a>
{% endif %} {% endif %}
</div> </div>
</header> </header>
@ -81,10 +82,9 @@
<p> <p>
FOSSDB is a passion project of <a class="underline" FOSSDB is a passion project of <a class="underline"
href="https://github.com/kristoferssolo" href="https://github.com/kristoferssolo"
target="_blank">@kristoferssolo</a> and a dedicated community of reporters. target="_blank">@kristoferssolo</a>.
</p> </p>
<!-- TODO: finish these sentences --> <!-- TODO: finish these sentences -->
<p>This site uses data from GitHub as well as data...</p>
<p>This site has no affiliation with GitHub or any other hosting platform.</p> <p>This site has no affiliation with GitHub or any other hosting platform.</p>
</div> </div>
<div></div> <div></div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,3 +1,11 @@
Array.from(document.getElementsByClassName("remove-btn")).forEach((button) => {
button.addEventListener("click", () => {
console.log("TRUE")
const hiddenDeleteInput = button.previousElementSibling
hiddenDeleteInput.value = "on"
button.parentElement.style.display = "none"
})
})
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
const FORM_VERIFY = document.getElementsByClassName("verify") const FORM_VERIFY = document.getElementsByClassName("verify")
const SUBMIT_BUTTON = document.getElementById("submit-button") const SUBMIT_BUTTON = document.getElementById("submit-button")
@ -10,30 +18,26 @@ window.addEventListener("DOMContentLoaded", () => {
) )
if (ALL_FILLED) { if (ALL_FILLED) {
SUBMIT_BUTTON.classList.remove( SUBMIT_BUTTON.classList.remove(
"bg-slategray-200", "submit-button-disabled",
"text-lightsteelblue-100", "text-lightsteelblue-100",
"opacity-50", "bg-slategray-200"
"cursor-default"
) )
SUBMIT_BUTTON.classList.add( SUBMIT_BUTTON.classList.add(
"bg-skyblue-300", "submit-button-enabled",
"text-gray-500", "text-gray-500",
"opacity-100", "bg-skyblue-300"
"hover:opacity-60"
) )
SUBMIT_BUTTON.disabled = false SUBMIT_BUTTON.disabled = false
} else { } else {
SUBMIT_BUTTON.classList.remove( SUBMIT_BUTTON.classList.remove(
"bg-skyblue-300", "submit-button-enabled",
"text-gray-500", "text-gray-500",
"opacity-100", "bg-skyblue-300"
"hover:opacity-60"
) )
SUBMIT_BUTTON.classList.add( SUBMIT_BUTTON.classList.add(
"bg-slategray-200", "submit-button-disabled",
"text-lightsteelblue-100", "text-lightsteelblue-100",
"opacity-50", "bg-slategray-200"
"cursor-default"
) )
SUBMIT_BUTTON.disabled = true SUBMIT_BUTTON.disabled = true
} }

24
tox.ini
View File

@ -1,24 +0,0 @@
[tox]
minversion = 3.8.0
envlist = django, flake8, mypy
isolated_build = true
[gh-actions]
python =
3.10: py310, mypy, flake8
[testenv:django]
basepython = python3.10
deps = django
commands = python manage.py test
[testenv:flake8]
basepython = python3.10
deps = flake8
commands = flake8 FOSSDB_web
[testenv:mypy]
basepython = python3.10
deps =
-r{toxinidir}/requirements_dev.txt
commands = mypy FOSSDB_web