diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..9dde9a5f9 Binary files /dev/null and b/.DS_Store differ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e0dc1bb4..b12c57925 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: rev: v0.1.13 hooks: - id: ruff - args: + args: - --fix - id: ruff-format diff --git a/blt/urls.py b/blt/urls.py index b30461814..77b135b76 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -164,6 +164,8 @@ invite_friend, profile, profile_edit, + recommend_user, + recommend_via_blurb, referral_signup, stripe_connected, update_bch_address, @@ -623,8 +625,9 @@ path("sizzle-docs/", sizzle_docs, name="sizzle-docs"), path("api/timelogsreport/", TimeLogListAPIView, name="timelogsreport"), path("time-logs/", TimeLogListView, name="time_logs"), + path("recommend//", recommend_user, name="recommend_user"), + path("recommend//blurb/", recommend_via_blurb, name="recommend_via_blurb"), path("sizzle-daily-log/", sizzle_daily_log, name="sizzle_daily_log"), - path("blog/", include("blog.urls")), path( "user-sizzle-report//", user_sizzle_report, diff --git a/website/admin.py b/website/admin.py index a811f2669..f1eb46c10 100644 --- a/website/admin.py +++ b/website/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from django.template.defaultfilters import truncatechars from django.utils import timezone @@ -23,6 +24,7 @@ Payment, Points, Project, + Recommendation, Subscription, Suggestion, SuggestionVotes, @@ -211,18 +213,26 @@ def short_description(self, obj): admin.site.unregister(User) -class UserAdmin(ImportExportModelAdmin): - resource_class = UserResource - list_display = ( - "id", - "username", - "email", - "first_name", - "last_name", - "is_active", - "date_joined", - "is_staff", - ) +# class UserAdmin(ImportExportModelAdmin): +# resource_class = UserResource +# list_display = ( +# "id", +# "username", +# "email", +# "first_name", +# "last_name", +# "is_active", +# "date_joined", +# "is_staff", +# ) + + +class RecommendationAdmin(admin.ModelAdmin): + list_display = ("recommender", "recommended_user", "created_at") + search_fields = ("recommender__username", "recommended_user__username") + + +admin.site.register(Recommendation, RecommendationAdmin) class UserProfileAdmin(admin.ModelAdmin): @@ -245,6 +255,8 @@ class UserProfileAdmin(admin.ModelAdmin): "flagged_count", "subscribed_domains_count", "subscribed_users_count", + "recommendation_count", + "recommendation_blurb", "x_username", "linkedin_url", "github_url", @@ -273,6 +285,9 @@ def subscribed_domains_count(self, obj): def subscribed_users_count(self, obj): return obj.subscribed_users.count() + def recommendation_count(self, obj): + return obj.recommendations.count() + class IssueScreenshotAdmin(admin.ModelAdmin): model = IssueScreenshot diff --git a/website/api/views.py b/website/api/views.py index acb11b7b1..d42cf92e5 100644 --- a/website/api/views.py +++ b/website/api/views.py @@ -3,6 +3,7 @@ from datetime import datetime from django.conf import settings +from django.contrib.auth.models import AnonymousUser from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.files.storage import default_storage @@ -68,12 +69,10 @@ class UserIssueViewSet(viewsets.ModelViewSet): http_method_names = ["get", "head"] def get_queryset(self): - anonymous_user = self.request.user.is_anonymous - user_id = self.request.user.id - if anonymous_user: - return Issue.objects.exclude(Q(is_hidden=True)) + if isinstance(self.request.user, AnonymousUser): + return Issue.objects.exclude(is_hidden=True) else: - return Issue.objects.exclude(Q(is_hidden=True) & ~Q(user_id=user_id)) + return Issue.objects.exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) class UserProfileViewSet(viewsets.ModelViewSet): diff --git a/website/forms.py b/website/forms.py index ace00fd8f..0ad82c037 100644 --- a/website/forms.py +++ b/website/forms.py @@ -1,3 +1,5 @@ +import re + from captcha.fields import CaptchaField from django import forms from mdeditor.fields import MDTextFormField @@ -26,13 +28,31 @@ class Meta: "discounted_hourly_rate", "github_url", "role", + "recommendation_blurb", ] widgets = { "tags": forms.CheckboxSelectMultiple(), "subscribed_domains": forms.CheckboxSelectMultiple(), "subscribed_users": forms.CheckboxSelectMultiple(), + "recommendation_blurb": forms.Textarea( + attrs={ + "rows": "10", + "class": "mt-2 block w-full py-3 px-4 text-base border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500", + "placeholder": "Write your recommendation blurb here...", + } + ), } + def clean_recommendation_blurb(self): + blurb = self.cleaned_data.get("recommendation_blurb") + # Remove any potential template tags or code that might have been entered + if blurb: + # Remove any HTML or template tags + blurb = re.sub(r"<[^>]+>", "", blurb) + blurb = re.sub(r"{%.*?%}", "", blurb) + blurb = re.sub(r"{{.*?}}", "", blurb) + return blurb + # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # print("UserProfileForm __init__") diff --git a/website/migrations/0154_recommendation_recommendation_unique_recommendation.py b/website/migrations/0154_recommendation_recommendation_unique_recommendation.py new file mode 100644 index 000000000..5a1456a75 --- /dev/null +++ b/website/migrations/0154_recommendation_recommendation_unique_recommendation.py @@ -0,0 +1,50 @@ +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0153_delete_contributorstats"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Recommendation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "recommended_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_recommendations", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "recommender", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="given_recommendations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="recommendation", + constraint=models.UniqueConstraint( + fields=("recommender", "recommended_user"), name="unique_recommendation" + ), + ), + ] diff --git a/website/migrations/0155_userprofile_recommendation_blurb.py b/website/migrations/0155_userprofile_recommendation_blurb.py new file mode 100644 index 000000000..43464c6f0 --- /dev/null +++ b/website/migrations/0155_userprofile_recommendation_blurb.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-21 19:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0154_recommendation_recommendation_unique_recommendation"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="recommendation_blurb", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/website/migrations/0156_userprofile_recommended_by.py b/website/migrations/0156_userprofile_recommended_by.py new file mode 100644 index 000000000..a55d0a86c --- /dev/null +++ b/website/migrations/0156_userprofile_recommended_by.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.3 on 2024-11-23 07:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0155_userprofile_recommendation_blurb"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="recommended_by", + field=models.ManyToManyField( + blank=True, related_name="has_recommended", to="website.userprofile" + ), + ), + ] diff --git a/website/migrations/0157_remove_recommendation_message_and_more.py b/website/migrations/0157_remove_recommendation_message_and_more.py new file mode 100644 index 000000000..8872d67ed --- /dev/null +++ b/website/migrations/0157_remove_recommendation_message_and_more.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2024-12-25 16:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0156_userprofile_recommended_by"), + ] + + operations = [ + migrations.AddField( + model_name="recommendation", + name="recommendation_message", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/website/models.py b/website/models.py index 3398c8e1c..400768246 100644 --- a/website/models.py +++ b/website/models.py @@ -509,6 +509,7 @@ class UserProfile(models.Model): title = models.IntegerField(choices=title, default=0) role = models.CharField(max_length=255, blank=True, null=True) description = models.TextField(blank=True, null=True) + recommendation_blurb = models.TextField(blank=True, null=True) winnings = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) issue_upvoted = models.ManyToManyField(Issue, blank=True, related_name="upvoted") issue_downvoted = models.ManyToManyField(Issue, blank=True, related_name="downvoted") @@ -516,6 +517,17 @@ class UserProfile(models.Model): issue_flaged = models.ManyToManyField(Issue, blank=True, related_name="flaged") issues_hidden = models.BooleanField(default=False) + recommended_by = models.ManyToManyField( + "self", symmetrical=False, related_name="has_recommended", blank=True + ) + + @property + def recommendation_count(self): + # Count both types of recommendations + standard_recommendations = Recommendation.objects.filter(recommended_user=self.user).count() + blurb_recommendations = self.recommended_by.count() + return standard_recommendations + blurb_recommendations + subscribed_domains = models.ManyToManyField( Domain, related_name="user_subscribed_domains", blank=True ) @@ -890,6 +902,29 @@ def __str__(self): return f"ActivityLog by {self.user.username} at {self.recorded_at}" +class Recommendation(models.Model): + recommender = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="given_recommendations" + ) + recommended_user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="received_recommendations" + ) + recommendation_message = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["recommender", "recommended_user"], name="unique_recommendation" + ) + ] + + def __str__(self): + return ( + f"Recommendation from {self.recommender.username} to {self.recommended_user.username}" + ) + + class DailyStatusReport(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) date = models.DateField() diff --git a/website/serializers.py b/website/serializers.py index a73857674..e20fb36cd 100644 --- a/website/serializers.py +++ b/website/serializers.py @@ -68,6 +68,7 @@ class Meta: "issue_flaged", "total_score", "activities", + # "recommendations", ) diff --git a/website/templates/profile.html b/website/templates/profile.html index bdda41c67..49d1b2f9b 100644 --- a/website/templates/profile.html +++ b/website/templates/profile.html @@ -269,6 +269,77 @@ color: #ff5722; font-weight: bold; } + + /* Added styles for Recommendations */ + .recommend-user { + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + } + .red-btn { + background-color: #cc0000; + color: #fff; + border: none; + margin : 10px; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; + } + .red-btn:hover { + background-color: #990000; + } + .error { + color: #cc0000; + font-weight: bold; + margin-bottom: 15px; + } + + #user_list { + max-height: 200px; + overflow-y: auto; + border: 1px solid #ddd; + padding: 10px; + border-radius: 5px; + } + + .user-item { + padding: 5px 0; + border-bottom: 1px solid #eee; + } + + .user-item:last-child { + border-bottom: none; + } + + .recommendation-section { + margin: 20px 0; + padding: 15px; + border: 1px solid #e0e0e0; + border-radius: 5px; +} + +.recommendation-blurb { + margin-bottom: 15px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 4px; +} + +.recommend-blurb-btn { + margin-top: 10px; +} + +.recommend-blurb-btn.recommended { + background-color: #dc3545; + border-color: #dc3545; +} + +.recommendation-count { + font-size: 14px; + padding: 5px 10px; +} {% endblock style %} {% block content %} @@ -304,20 +375,74 @@ {% endif %} -
- {{ my_score|default:0 }} {% trans "POINTS" %} - {% if request.user != user and request.user.is_authenticated %} - - {% if request.user.email in followers_list %} - - {% else %} - - {% endif %} - {% endif %} +
+ +
+ + {{ my_score|default:0 }} + {% trans "POINTS" %} + +
+ + + +
+

+ Streak Details +

+
+ +
+ Current Streak: + {{ user.userprofile.current_streak|default:"0" }} +
+
+ +
+ Longest Streak: + {{ user.userprofile.longest_streak|default:"0" }} +
+
+ +
+ Last Sizzle Check-in: + {{ user.userprofile.last_check_in|default:"N/A" }} +
+
{% if request.user == user %} @@ -494,6 +619,14 @@ + + @@ -510,11 +643,250 @@ {% endfor %}
+
+ About Me +
+
+ +
+ {% if object.userprofile.recommendation_blurb %} +

{{ object.userprofile.recommendation_blurb }}

+ {% else %} +

No recommendation blurb yet

+ {% endif %} + {% if request.user.is_authenticated and request.user != object %} +
+ + +
+ {% endif %} + +
+
Monthly Report
+ {% if request.user == user %} +
+
Spot a Talent? Recommend Now!
+
+ {% csrf_token %} + + +
+ {% for user in all_users %} +
+ + +
+ {% endfor %} +
+ +
+
+ {% endif %}
@@ -596,6 +968,9 @@

Followed By:

  {{ user.username }} + + {{ recommendation.created_at|date:"F j, Y" }} + {% endfor %} @@ -671,6 +1046,68 @@

Following:

+