Login page moved to forms.py, New warning and ban system, a bunch of other shit.

- Change password page from profile settings has been moved over to forms.py
- Login page has been moved over to forms.py. There is no JavaScript right now, however it is still functional.
-Profile_tools_Form has been altered to remove a few unneeded fields that administrators don't need to mess with.
- warned_reason and warned fields are gone from User_tools_Form and will be removed from models.py at a later point.

- a new ban system has been implemented. While bans are still incomplete and lack capabilities to ban IP addresses as of right now, the groundwork is now here and those features can be added later.
- manage_bans page was added.

- A new warning system has been implemented. If a Warning is created, a notification is created automatically with it.  A warning notification replaces the "new announcement" notification type.
- Warning notifications are larger and stand out compared to normal notifications
- When posting, commenting or messaging someone, a check is performed to make sure you've seen the warning notification.
- manage_warnings page was added. This page will also show you the warning history.

- help_text was added to a bunch of fields.
This commit is contained in:
some weird guy
2023-08-23 16:47:28 -07:00
parent e190d76079
commit ca09f3295b
44 changed files with 652 additions and 543 deletions
+29 -11
View File
@@ -1,21 +1,39 @@
Stuff that is done:
- Disable comments button
- merge with closedverse-video-support (mostly)
- Prevent users from posting in communities created by blocked users.
- Hide email password reset form if no SMTP server is set up. (done by Arian Kordi)
- Fix icon and banner uploads.
Icons and banners use util.image_upload now, at the cost of having to set them all again.
Stuff that is in progress
- Removing shitty code.
- Adding help_text, and placeholders for clarity, this will be seen on the admin panel and even forms too.
- moving to forms.py
So far, user_tools and community_tools have been moved over.
- New Warning system.
Warnings are here, And there's an interface for warning users.
When a warning is made, a notification is also made.
Replaced the 5th notification type with warnings instead of announcements.
A check is made to make sure request.user does not have unread warnings before they can post.
There's no javascript for the form page yet.
Audit logs are not set up, but the Warning model shows who warned each user.
Stuff that I want
- User friendly interface for admins
Admins should be able to manage users quickly and effectively without having to worry about a clusterfuck interface.
Primary actions like warning users, banning users, and whatnot should be an easy few clicks away.
- New Ban system.
Bans are here also with its own interface.
Instead of entering a datetime field manually, Admins are provided with choises for different ban lengths when using the frontend.
IP bans and range bans are not added yet.
A proper ban page is not added yet and simply shows a 403 error.
If a ban page is added, it should show the reason, and when the ban will expire.
There's no javascript for the form page yet.
Audit logs are not set up, but the Ban model shows who banned each user.
- Removing shitty code.
- moving to forms.py.
The login page, admin tools, and community tools are using forms.py.
Ideas:
- Image and video file boxes in one.
- Pinning posts????
Not needed at all if I'm being honest.
- A new user metadata page that does not suck.
- Make it, so you need to enter your current password to change your email address.
- Ways for mods to view who invited who.
- Full ImageField integration
- Full ImageField integration.
- Filefield may have to be used for both photos and videos.
- remove the useless feedback thing. (You can just make a bug reporting community)
+9 -51
View File
@@ -29,7 +29,7 @@ DEBUG = False
# Do not include 127.0.0.1 or localhost
# in production for security reasons.
ALLOWED_HOSTS = [
'127.0.0.1',
'',
]
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
@@ -63,10 +63,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
#'ban.middleware.BanManagement',
'closedverse_main.middleware.ClosedMiddleware',
#'maintenance.middleware.MaintenanceManagement',
'closedverse_main.middleware.CheckForBanMiddleware',
]
if not DEBUG:
MIDDLEWARE += ['whitenoise.middleware.WhiteNoiseMiddleware']
@@ -103,30 +101,6 @@ DATABASES = {
}
}
"""
# log errors.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': 'logs/errors.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
"""
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@@ -168,15 +142,6 @@ USE_L10N = False
USE_TZ = True
# You can use Mailtrap to get the email system working. Mailtrap is free and will work well for what you'll be doing with this.
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = None
EMAIL_HOST_USER = None
EMAIL_HOST_PASSWORD = None
EMAIL_PORT = None
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = None
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
@@ -246,7 +211,7 @@ RECAPTCHA_PUBLIC_KEY = None
RECAPTCHA_PRIVATE_KEY = None
# Key for IPHub service for Closedverse, which detects proxies.
IPHUB_KEY = ""
IPHUB_KEY = None
# If this is set to True, then users will receive an error
# upon trying to sign up for the site behind a proxy.
# Uses IPHub service and requires an API key defined above.
@@ -282,25 +247,17 @@ invite_only = False
# Minimum level required to view IP addresses and user agents. (default: 10)
# Mods under this level will still be able to manage users, however will not be able to view any sensitive data.
min_lvl_metadata_perms = 100
# Set this to 0 to prevent anyone from viewing data.
min_lvl_metadata_perms = 0
# if someone's level is equal or above this, they can edit most community on your clone.
# if someone's level is equal or above this, they can edit the most communities on your clone.
level_needed_to_man_communities = 5
# if someone's level is equal or above this, they can edit any user with a lower level on your clone.
level_needed_to_man_users = 5
<<<<<<< Updated upstream
# file size limits in megabytes! only applies when using the community tools.
max_icon_size = .5
max_banner_size = 1
# minimum_password_length is removed as the AUTH_PASSWORD_VALIDATORS work as of 08/2023
# The minimum length required for a user's password. This is to save the users from themselves in the event of a data breach. The longer and more complex the password is, the harder it is to be cracked. (default: 7)
# This will definitely miss a few people off who just want to sign up without worrying about long passwords.
minimum_password_length = 7
=======
>>>>>>> Stashed changes
# The hard limit for uploading, Will cause an error if this is exceeded. This is set to 15MB by default (15728640)
DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800
@@ -323,7 +280,8 @@ memo_msg = """
<h2>Cedar</h2>
<p>what</p>
<h2>Why is this person rehosting it?</h2>
<p>im bored</p>"""
<p>im bored</p>
"""
# This option enables some production-specific pages
# and routines, such as HTTPS scheme redirection and
+29 -32
View File
@@ -21,12 +21,12 @@ class UserForm(ModelForm):
'password': PasswordInput(),
}
"""
@admin.action(description='Hide selected items')
def Hide_Memo(modeladmin, request, queryset):
queryset.update(show = False)
@admin.action(description='Show selected items')
def Show_Memo(modeladmin, request, queryset):
queryset.update(show = True)
@admin.action(description='Void selected Invites')
def Void_invite(modeladmin, request, queryset):
queryset.update(void = True)
@admin.action(description='Restore selected Invites')
def Restore_invite(modeladmin, request, queryset):
queryset.update(void = False, used = False)
@admin.action(description='Hide selected items')
def Hide_content(modeladmin, request, queryset):
queryset.update(is_rm = True)
@@ -60,7 +60,7 @@ def Enable_user(modeladmin, request, queryset):
class UserAdmin(admin.ModelAdmin):
search_fields = ('id', 'unique_id', 'username', 'nickname', 'email', )
search_fields = ('id', 'username', 'nickname', 'email', )
list_display = ('id', 'username', 'nickname', 'warned', 'level', 'staff', 'active', )
exclude = ('addr', 'signup_addr', 'password', )
actions = [Disable_user, Enable_user]
@@ -68,20 +68,27 @@ class UserAdmin(admin.ModelAdmin):
# Not yet
#form = UserForm
class ProfileAdmin(admin.ModelAdmin):
search_fields = ('id', 'unique_id', 'origin_id', )
search_fields = ('id', 'user__username__icontains', 'comment', 'origin_id',)
raw_id_fields = ('user', 'favorite',)
list_display = ('id', 'user', 'comment', 'let_freedom',)
class InvitesAdmin(admin.ModelAdmin):
search_fields = ('creator__username', 'used_by__username', 'code', )
raw_id_fields = ('creator', 'used_by', )
list_display = ('creator', 'used_by', 'code', 'used', 'void', )
actions = [Void_invite, Restore_invite]
class ComplaintAdmin(admin.ModelAdmin):
search_fields = ('id', 'unique_id', 'body', )
search_fields = ('id', 'body', )
raw_id_fields = ('creator', )
class ConversationAdmin(admin.ModelAdmin):
search_fields = ('id', 'unique_id', )
search_fields = ('id', )
raw_id_fields = ('source', 'target', )
class PostAdmin(admin.ModelAdmin):
raw_id_fields = ('creator', 'poll', )
search_fields = ('id', 'unique_id', 'body', 'creator__username', )
search_fields = ('id', 'body', 'creator__username', )
list_display = ('id', 'creator', 'body', 'is_rm', )
actions = [Hide_content, Show_content, Disable_comments, Enable_comments]
def get_queryset(self, request):
@@ -89,7 +96,7 @@ class PostAdmin(admin.ModelAdmin):
class CommentAdmin(admin.ModelAdmin):
raw_id_fields = ('creator', 'original_post', )
search_fields = ('id', 'unique_id', 'body', 'creator__username', )
search_fields = ('id', 'body', 'creator__username', )
list_display = ('id', 'creator', 'body', 'original_post', 'is_rm', )
actions = [Hide_content, Show_content]
def get_queryset(self, request):
@@ -98,14 +105,14 @@ class CommentAdmin(admin.ModelAdmin):
class CommunityAdmin(admin.ModelAdmin):
raw_id_fields = ('creator', )
list_display = ('id', 'name', 'description', 'type', 'creator', 'popularity', 'is_rm', 'is_feature', 'require_auth')
search_fields = ('id', 'unique_id', 'name', 'description', )
search_fields = ('id', 'name', 'description', )
actions = [Hide_content, Show_content, Feature_community, Unfeature_community, force_login, unforce_login]
def get_queryset(self, request):
return models.Community.real.get_queryset()
class MessageAdmin(admin.ModelAdmin):
raw_id_fields = ('creator', 'conversation', )
search_fields = ('id', 'unique_id', 'body', 'creator__username', )
search_fields = ('id', 'body', 'creator__username', )
list_display = ('id', 'creator', 'conversation', 'body', )
actions = [Hide_content, Show_content]
def get_queryset(self, request):
@@ -113,19 +120,16 @@ class MessageAdmin(admin.ModelAdmin):
class NotificationAdmin(admin.ModelAdmin):
raw_id_fields = ('to', 'source', 'context_post', 'context_comment',)
search_fields = ('unique_id', )
search_fields = ('to__username', 'source__username', 'context_post__body', 'context_comment__body',)
list_display = ('id', 'to', 'source', 'context_post', 'context_comment',)
class AuditAdmin(admin.ModelAdmin):
raw_id_fields = ('by', 'user', 'post', 'comment', 'reversed_by', )
search_fields = ('by__username', 'user__username', )
raw_id_fields = ('by', 'user', 'post', 'comment', 'community', 'reversed_by', )
search_fields = ('by__username', 'user__username', 'post__body', 'comment__body', 'community__name', )
class AdsAdmin(admin.ModelAdmin):
raw_id_fileds = ('id', 'created', 'url', 'imageurl')
class InvitesAdmin(admin.ModelAdmin):
raw_id_fileds = ('id', 'created', 'creator')
class YeahAdmin(admin.ModelAdmin):
raw_id_fields = ('by', 'post', 'comment', )
list_display = ('by', 'post', 'comment', )
@@ -145,11 +149,13 @@ admin.site.unregister(Group)
admin.site.register(models.Role, RoleAdmin)
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Profile, ProfileAdmin)
admin.site.register(models.Invites, InvitesAdmin)
admin.site.register(models.Community, CommunityAdmin)
admin.site.register(models.Complaint, ComplaintAdmin)
admin.site.register(models.Message, MessageAdmin)
admin.site.register(models.Conversation, ConversationAdmin)
admin.site.register(models.Notification, NotificationAdmin)
#admin.site.register(models.LoginAttempt, LoginAdmin)
admin.site.register(models.UserBlock)
admin.site.register(models.AuditLog, AuditAdmin)
admin.site.register(models.ProfileHistory, HistoryAdmin)
@@ -157,17 +163,9 @@ admin.site.register(models.ProfileHistory, HistoryAdmin)
admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Comment, CommentAdmin)
<<<<<<< Updated upstream
admin.site.register(models.Ads, AdsAdmin)
admin.site.register(models.welcomemsg, WelcomemsgAdmin)
admin.site.register(models.Yeah, YeahAdmin)
admin.site.register(models.Follow)
admin.site.register(models.FriendRequest)
admin.site.register(models.Friendship, ConversationAdmin)
admin.site.register(models.Invites, InvitesAdmin)
admin.site.register(models.Poll)
admin.site.register(models.PollVote)
=======
admin.site.register(models.Ban)
admin.site.register(models.Warning)
if settings.DEBUG:
admin.site.register(models.Yeah)
@@ -178,4 +176,3 @@ if settings.DEBUG:
admin.site.register(models.Poll)
admin.site.register(models.PollVote)
admin.site.register(models.Ads, AdsAdmin)
>>>>>>> Stashed changes
+84 -8
View File
@@ -1,6 +1,9 @@
from django import forms
from .models import *
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.utils.timezone import timedelta
from closedverse import settings
# I do want to move each and every form over to here. Not only will this trivialize making new forms, this will also make it more secure or something.
class CommunitySettingForm(forms.ModelForm):
@@ -21,20 +24,61 @@ class CommunitySettingForm(forms.ModelForm):
'community_banner'
)
class Settomgs_Change_Password(forms.Form):
Old_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Old Password'}))
New_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'New Password'}))
Confirm_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Confirm Password'}))
def clean(self):
cleaned_data = super().clean()
old = cleaned_data.get('Old_Password')
new = cleaned_data.get('New_Password')
confirm = cleaned_data.get('Confirm_Password')
if not old or not new or not confirm:
raise forms.ValidationError('Please fill out all the fields.')
if old == new:
raise forms.ValidationError('The old password and new password can\'t be the same.')
if new != confirm:
raise forms.ValidationError('Passwords do not match.')
try:
validate_password(new)
except forms.ValidationError as error:
raise forms.ValidationError("your password fucking sucks!")
return cleaned_data
class PurgeForm(forms.Form):
purge_posts = forms.BooleanField(required=False, label='Purge all posts', help_text='Purge all posts from this user.')
purge_comments = forms.BooleanField(required=False, label='Purge all comments', help_text='Purge all comments from this user.')
restore_all = forms.BooleanField(required=False, label='Restore purged content', help_text='Restore everything that was purged, this will not apply to posts deleted manually.')
class UserForm(forms.ModelForm):
class LoginForm(forms.Form):
username = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'auth-input', 'placeholder': 'Username'}))
password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Password'}))
def clean(self):
cleaned_data = super(LoginForm, self).clean()
username = cleaned_data.get('username')
password = cleaned_data.get('password')
user = User.objects.authenticate(username=username, password=password)
if user is None:
raise forms.ValidationError("The user doesn't exist.")
elif user[1] == False:
raise forms.ValidationError("Invalid password.", code='invalid')
elif user[1] == 2:
raise forms.ValidationError("This account's password needs to be reset. Contact an admin or reset by email.", code='required_reset')
elif not user[0].is_active():
raise forms.ValidationError("This account was disabled.", code='disabled')
return cleaned_data
class User_tools_Form(forms.ModelForm):
username = forms.CharField(max_length = 64, required = True, help_text = 'Usernames are used when signing in, so be sure to let this user know if you change this.')
c_tokens = forms.IntegerField(min_value = 0, max_value = 100, required = False, help_text = 'The remaining C-Tokens this user has.')
has_mh = forms.BooleanField(required = False, label='Use Mii', help_text='Don\'t fuck with this please.')
avatar = forms.CharField(required = False, help_text = 'If \"Use Mii\" is turned on, The Mii ID should reside here, otherwise any good old URL will do.')
has_mh = forms.BooleanField(required = False, label='Use Mii', help_text='Turn this on to use Mii Hashes instead of normal profile pictures.')
avatar = forms.CharField(required = False, help_text = 'If \"Use Mii\" is turned on, The Mii Hash should reside here, otherwise any good old URL will do.')
class Meta:
model = User
fields = ['username', 'nickname', 'role', 'c_tokens', 'hide_online', 'active', 'can_invite', 'warned', 'has_mh', 'avatar', 'warned_reason']
fields = ['username', 'nickname', 'role', 'c_tokens', 'hide_online', 'active', 'can_invite', 'has_mh', 'avatar']
def clean_username(self):
username = self.cleaned_data.get('username')
@@ -42,11 +86,43 @@ class UserForm(forms.ModelForm):
raise forms.ValidationError("The username contains invalid characters.")
return username
class ProfileForm(forms.ModelForm):
class Profile_tools_Form(forms.ModelForm):
# applying classes is super weird.
comment = forms.CharField(required=False, widget=forms.Textarea(attrs={'class': 'textarea'}))
let_freedom = forms.BooleanField(required=False, label='Let this user upload images?', help_text='If you disable this, This user will not be able to post images, videos or make a new account.')
class Meta:
model = Profile
fields = ['comment', 'country', 'whatareyou', 'weblink', 'external', 'let_freedom', 'limit_post', 'cannot_edit', 'pronoun_is', 'id_visibility', 'let_friendrequest', 'yeahs_visibility', 'comments_visibility']
fields = ['comment', 'country', 'whatareyou', 'weblink', 'external', 'let_freedom', 'limit_post', 'cannot_edit']
class Give_warning_form(forms.ModelForm):
reason = forms.CharField(required=True, widget=forms.Textarea(attrs={'class': 'textarea'}))
# Super simple, does not need to be it's own form but whatever.
class Meta:
model = Warning
fields = ['reason']
class Give_Ban_Form(forms.ModelForm):
BAN_OPTIONS = [
(1, '1 Day'),
(2, '2 Days'),
(3, '3 Days'),
(7, '1 Week'),
(14, '2 Weeks'),
(30, '1 Month'),
(60, '2 Months'),
(365, '1 Year'),
(None, 'Forever'),
]
# In the future we can add options for IP bans and shit.
reason = forms.CharField(required=True, widget=forms.Textarea(attrs={'class': 'textarea'}))
expiry_date = forms.ChoiceField(required=False, choices=BAN_OPTIONS, initial=1)
def clean_expiry_date(self):
expiry_choice = self.cleaned_data['expiry_date']
if expiry_choice:
return timezone.now() + timedelta(days=int(expiry_choice))
else:
return timezone.now() + timedelta(days=365.25*100) # same exact thing done in indigo.
class Meta:
model = Ban
fields = ['reason', 'expiry_date']
+27
View File
@@ -2,6 +2,8 @@ from django.http import HttpResponseForbidden
from closedverse import settings
from django.shortcuts import redirect
from django.contrib.auth import logout
from .models import Ban
from django.utils import timezone
from re import compile
# Taken from https://python-programming.com/recipes/django-require-authentication-pages/
@@ -10,6 +12,31 @@ if settings.FORCE_LOGIN:
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
EXEMPT_URLS += [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS]
class CheckForBanMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
# If the user is not authenticated, there's no need to check for bans
if not request.user.is_authenticated:
return None
# Get one active ban that is not expired
active_ban = Ban.objects.filter(
to=request.user,
active=True,
expiry_date__gte=timezone.now()
).first()
if active_ban:
return HttpResponseForbidden('You are banned from this site. Reason: ' + active_ban.reason)
return None
class ClosedMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
+87 -74
View File
@@ -42,13 +42,12 @@ class UserManager(BaseUserManager):
user.save(using=self._db)
return user
def closed_create_user(self, username, password, email, addr, signup_addr, user_agent, nick, nn, gravatar):
def closed_create_user(self, username, password, email, addr, signup_addr, nick, nn, gravatar):
user = self.model(
username = username,
nickname = util.filterchars(nick),
addr = addr,
signup_addr = signup_addr,
user_agent = user_agent,
email = email,
)
profile = Profile.objects.model()
@@ -125,7 +124,7 @@ class Role(models.Model):
id = models.AutoField(primary_key=True)
# determines whether to fetch role from static or media, for built-in roles
is_static = models.BooleanField(default=False)
image = models.ImageField(upload_to='roles/', max_length=100, help_text='Upload an icon that will show on the top right of one\'s profile.')
image = models.ImageField(upload_to='roles/', max_length=100, help_text='Upload an icon that will show on the top left of one\'s profile. A 22x22 image works best!')
organization = models.CharField(max_length=255, blank=True, null=True, help_text='Text that shows above one\'s username')
def __str__(self):
@@ -143,39 +142,26 @@ class User(AbstractBaseUser):
nickname = models.CharField(max_length=64, null=True)
password = models.CharField(max_length=128)
email = models.EmailField(null=True, blank=True, default='')
has_mh = models.BooleanField(default=False)
has_mh = models.BooleanField(default=False, help_text='Don\'t touch this. This is if the user\'s avatar is set to a Mii.')
avatar = models.CharField(max_length=1200, blank=True, default='')
theme = ColorField(blank=True, null=True)
# LEVEL: 0-1 is default, everything else is just levels
level = models.SmallIntegerField(default=0)
# ROLE: This doesn't have anything
<<<<<<< Updated upstream
role = models.SmallIntegerField(default=0, choices=((0, 'normal'), (1, 'bot'), (2, 'administrator'), (3, 'moderator'), (4, 'openverse'), (5, 'donator'), (6, 'cool'), (7, 'urapp'), (8, 'owner'), (9, 'badgedes'), (10, 'jack'), (11, 'verified'),))
=======
#role = models.SmallIntegerField(default=0, choices=((0, 'normal'), (1, 'bot'), (2, 'administrator'), (3, 'moderator'), (4, 'openverse'), (5, 'donator'), (6, 'cool'), (7, 'urapp'), (8, 'owner'), (9, 'badgedes'), (10, 'jack'), (11, 'verified'),))
level = models.SmallIntegerField(default=0, help_text='The rank of the user, \"0\" is by default. Users with a high enough rank will be able to manage users.')
role = models.ForeignKey(Role, blank=True, null=True, on_delete=models.SET_NULL, help_text='This will show a funny badge and text on this user\'s profile. This does not grant the user any additional power and is only visual.')
>>>>>>> Stashed changes
addr = models.CharField(max_length=64, null=True, blank=True)
signup_addr = models.CharField(max_length=64, null=True, blank=True)
user_agent = models.TextField(null=True, blank=True)
# C Tokens are things that let you make communities and shit.
<<<<<<< Updated upstream
c_tokens = models.IntegerField(default=1)
protect_data = models.BooleanField(default=False)
=======
c_tokens = models.IntegerField(default=1, help_text='How many communities should this user be allowed to make?')
protect_data = models.BooleanField(default=False, null=True, help_text='Prevent people from lookin\' at private data for this user.')
>>>>>>> Stashed changes
# Things that don't have to do with auth lol
hide_online = models.BooleanField(default=False)
hide_online = models.BooleanField(default=False, help_text='If this is ticked, the user has opted to hide their online status.')
color = ColorField(default='', null=True, blank=True)
staff = models.BooleanField(default=False)
active = models.BooleanField(default=True)
can_invite = models.BooleanField(default=True)
warned = models.BooleanField(default=False)
warned_reason = models.CharField(blank=True, null=True, max_length=600)
staff = models.BooleanField(default=False, help_text='Allow this user to access the admin panel you\'re using right now?')
active = models.BooleanField(default=True, help_text='If this is off, the user is basically banned and can\'t do shit here')
can_invite = models.BooleanField(default=True, help_text='Can this user invite new users? This does not matter unless the invite system is turned on.')
warned = models.BooleanField(default=False, help_text='Shows an annoying fucking warning box on the front page.')
warned_reason = models.CharField(blank=True, null=True, max_length=600, help_text='This string will show if the user is banned, or warned.')
bg_url = models.CharField(max_length=300, null=True, blank=True)
is_anonymous = False
@@ -209,12 +195,6 @@ class User(AbstractBaseUser):
return self.warned
def get_warned_reason(self):
return self.warned_reason
"""
def set_password(self, raw_password):
self.password = bcrypt_sha256.using(rounds=13).hash(raw_password)
def check_password(self, raw_password):
return bcrypt_sha256.using(rounds=13).verify(raw_password, hash=self.password)
"""
def profile(self, thing=None):
# If thing is specified, that field is retrieved
if thing:
@@ -236,6 +216,8 @@ class User(AbstractBaseUser):
except:
return None
return infodecode[0]
def unread_warning(self):
return Notification.objects.filter(type=5, to=self, read=False).exists()
def has_plain_avatar(self):
if not self.has_mh and '/' in self.avatar and not 'gravatar.com' in self.avatar:
return True
@@ -261,25 +243,8 @@ class User(AbstractBaseUser):
return int(limit) - recent_posts
def get_class(self):
"""
first = {
1: '/s/img/bot.png',
2: '/s/img/administrator.png',
3: '/s/img/moderator.png',
9: '/s/img/badgedes.png',
}.get(self.role, '')
second = {
1: "Bot",
2: "Administrator",
3: "Moderator",
9: "Badge Designer",
}.get(self.role, '')
"""
#if first:
# first = 'official ' + first
if not self.role:
return [None, None]
#first = self.role.image
second = self.role.organization
first = self.role.image.url
if self.role.is_static:
@@ -429,7 +394,7 @@ class User(AbstractBaseUser):
def notification_read(self):
return self.notification_to.filter(read=False).update(read=True)
def get_notifications(self):
return self.notification_to.select_related('context_post').select_related('context_comment').select_related('source').filter().order_by('-latest')[0:64]
return self.notification_to.select_related('context_post').select_related('context_comment').select_related('context_warning').select_related('source').filter().order_by('-latest')[0:64]
def notifications_clean(self):
""" Broken - gives OperationError on MySQL
notif_get = self.notification_to.all().values_list('id', flat=True)
@@ -617,6 +582,43 @@ class User(AbstractBaseUser):
return False
return user
# This will be a fucking pain in the ass!
class Ban(models.Model):
id = models.AutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
by = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE, related_name='banned_by')
to = models.ForeignKey(User, on_delete=models.CASCADE, related_name='banned_user')
reason = models.TextField(null=True, blank=True)
expiry_date = models.DateTimeField(help_text='The date and time on which this ban will expire, Set this way off into the future if this ban should be permanent')
active = models.BooleanField(default=True, help_text='Untick this to disable this ban')
def is_expired(self):
if timezone.now() > self.expiry_date:
return True
return False
def __str__(self):
return "Ban for " + str(self.to)
# The new warning system, Warnings do not contribute to bans, they just show as warnings.
# Can also be used as a means of reporting incident history.
class Warning(models.Model):
id = models.AutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='Warned_by', help_text="This is not shown to the recipient")
to = models.ForeignKey(User, on_delete=models.CASCADE, related_name='Warned_user')
reason = models.TextField(null=True, blank=True)
def save(self, *args, **kwargs):
# check if the object is being created or updated
is_new = self.pk is None
super().save(*args, **kwargs)
# create new notification only after new warning is created
if is_new:
Notification.give_notification(user=self.by, type=5, to=self.to, warning=self)
def __str__(self):
return "Warning for " + str(self.to)
# An invite system, for closed off communities or for whatever reason.
class Invites(models.Model):
id = models.AutoField(primary_key=True)
@@ -640,21 +642,20 @@ class Invites(models.Model):
class Community(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, default='')
ico = models.ImageField(upload_to='icon/%y/%m/%d/', max_length=100, blank=True, null=True)
banner = models.ImageField(upload_to='banner/%y/%m/%d/', max_length=100, blank=True, null=True)
description = models.TextField(blank=True)
ico = models.ImageField(null=True, blank=True)
banner = models.ImageField(null=True, blank=True)
# Type: 0 - general, 1 - game, 2 - special
type = models.SmallIntegerField(default=0, choices=((0, 'General'), (1, 'Game'), (2, 'Special'), (3, 'User Community'), (4, 'Hide')))
# Platform - 0/none, 1/3DS, 2/Wii U, 3/both
type = models.SmallIntegerField(default=0, help_text='The category the community belongs in, setting this to None will remove the community.', choices=((0, 'General'), (1, 'Game'), (2, 'Special'), (3, 'User Community'), (4, 'Hide')))
platform = models.SmallIntegerField(default=0, choices=((0, 'none'), (1, '3ds'), (2, 'wii u'), (3, 'switch'), (4, 'both'), (5, 'PC'), (6, 'Xbox'), (7, 'Playstation')))
tags = models.CharField(blank=True, null=True, max_length=255, choices=(('announcements', 'main announcement community'), ('changelog', 'main changelog'), ('activity', 'Activity Feed posting community'), ('general', 'General Discussion Community')))
tags = models.CharField(blank=True, help_text='Provides special functionality for specific communities.', null=True, max_length=255, choices=(('announcements', 'main announcement community'), ('changelog', 'main changelog'), ('activity', 'Activity Feed posting community'), ('general', 'General Discussion Community')))
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
is_rm = models.BooleanField(default=False)
is_feature = models.BooleanField(default=False)
require_auth = models.BooleanField(default=False)
rank_needed_to_post = models.IntegerField(default=0)
creator = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
is_rm = models.BooleanField(default=False, help_text='Tick this on to remove the community.')
is_feature = models.BooleanField(default=False, help_text='Feature this community on the front page where everyone can see it.')
require_auth = models.BooleanField(default=False, help_text='Force your users to sign in to view this community.')
rank_needed_to_post = models.IntegerField(default=0, help_text='Make admin only communities.')
creator = models.ForeignKey(User, blank=True, null=True, help_text='Who owns this community? It\'s highly recommended to not fill this on for general communities. Keep in mind that owners can block others from using communities they own.', on_delete=models.CASCADE)
objects = PostManager()
real = models.Manager()
@@ -668,7 +669,7 @@ class Community(models.Model):
def __str__(self):
return self.name
def icon(self):
if self.ico and hasattr(self.ico, 'url'):
if self.ico:
return self.ico.url
else:
return settings.STATIC_URL + "img/title-icon-default.png"
@@ -802,6 +803,8 @@ class Community(models.Model):
return 6
if not request.user.is_active():
return 6
if request.user.unread_warning():
return 13
if len(request.POST['body']) > 2200 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'):
return 1
upload = None
@@ -866,14 +869,14 @@ class Post(models.Model):
url = models.URLField(max_length=1200, null=True, blank=True, default='')
spoils = models.BooleanField(default=False)
disable_yeah = models.BooleanField(default=False)
lock_comments = models.SmallIntegerField(default=0, choices=((0, 'Not locked'), (1, 'Locked by user'), (2, 'Locked by mod')))
lock_comments = models.SmallIntegerField(default=0, choices=((0, 'Not locked'), (1, 'Locked by user'), (2, 'Locked by mod')), help_text='People will not be able to comment on posts that are locked.')
created = models.DateTimeField(auto_now_add=True)
edited = models.DateTimeField(auto_now=True)
befores = models.TextField(null=True, blank=True)
poll = models.ForeignKey('Poll', null=True, blank=True, on_delete=models.CASCADE)
befores = models.TextField(null=True, blank=True, help_text='If this post gets edited, the old body will be here.')
poll = models.ForeignKey('Poll', null=True, blank=True, on_delete=models.CASCADE, help_text='A busted ass poll feature that does not work.')
has_edit = models.BooleanField(default=False)
is_rm = models.BooleanField(default=False)
status = models.SmallIntegerField(default=0, choices=post_status)
is_rm = models.BooleanField(default=False, help_text='Tick this to hide this post')
status = models.SmallIntegerField(default=0, choices=post_status, help_text='Used to distinguish how a post was removed. Our purging feature uses this to determine what posts to restore and what not to.')
creator = models.ForeignKey(User, on_delete=models.CASCADE)
objects = PostManager()
@@ -1044,6 +1047,8 @@ class Post(models.Model):
return 6
if len(request.POST['body']) > 2200 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'):
return 1
if request.user.unread_warning():
return 13
upload = None
drawing = None
if request.FILES.get('screen'):
@@ -1145,8 +1150,8 @@ class Comment(models.Model):
edited = models.DateTimeField(auto_now=True)
befores = models.TextField(null=True, blank=True)
has_edit = models.BooleanField(default=False)
is_rm = models.BooleanField(default=False)
status = models.SmallIntegerField(default=0, choices=post_status)
is_rm = models.BooleanField(default=False, help_text='Tick this to hide this comment')
status = models.SmallIntegerField(default=0, choices=post_status, help_text='Used to distinguish how a comment was removed. Our purging feature uses this to determine what comments to restore and what not to.')
creator = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
objects = PostManager()
@@ -1278,8 +1283,8 @@ class Profile(models.Model):
#birthday = models.DateField(null=True, blank=True)
id_visibility = models.SmallIntegerField(default=0, choices=visibility)
pronoun_is = models.IntegerField(default=0, choices=(
(0, "I don't know"), (1, "He/him"), (2, "She/her"), (3, "He/she"), (4, "It"), (5, "They/Them")
pronoun_is = models.IntegerField(default=0, help_text='haha attack helicopter.', choices=(
(0, "Not specified"), (1, "He/him"), (2, "She/her"), (3, "He/she"), (4, "It"), (5, "They/Them")
))
let_friendrequest = models.SmallIntegerField(default=0, choices=visibility)
@@ -1292,11 +1297,11 @@ class Profile(models.Model):
favorite = models.ForeignKey(Post, blank=True, null=True, on_delete=models.SET_NULL)
let_yeahnotifs = models.BooleanField(default=True)
let_freedom = models.BooleanField(default=True)
let_freedom = models.BooleanField(default=True, help_text='Restrict this user from posting images, videos, URLs, and even making new accounts.')
# Todo: When you see this, implement it; make it a bool that determines whether the user should be able to edit their avatar; if this is true and
#let_avatar = models.BooleanField(default=False)
# Post limit, 0 for none
limit_post = models.SmallIntegerField(default=0)
limit_post = models.SmallIntegerField(default=0, help_text='Great for spammers, set to \"0\" to remove the restriction.')
# If this is true, the user can't change their avatar or nickname
cannot_edit = models.BooleanField(default=False, help_text='Make it so this user cannot change settings.')
email_login = models.SmallIntegerField(default=1, choices=((0, 'Do not allow'), (1, 'Okay'), (2, 'Only allow')))
@@ -1389,11 +1394,12 @@ class Notification(models.Model):
(2, 'Comment on my post'),
(3, 'Comment on others\' post'),
(4, 'Follow to me'),
(5, 'New Announcement'),
(5, 'Warning received'),
))
merges = models.TextField(blank=True, default='')
context_post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE)
context_comment = models.ForeignKey(Comment, null=True, blank=True, on_delete=models.CASCADE)
context_warning = models.ForeignKey(Warning, null=True, blank=True, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
latest = models.DateTimeField(auto_now=True)
@@ -1401,6 +1407,8 @@ class Notification(models.Model):
def __str__(self):
return "Notification from " + str(self.source) + " to " + str(self.to) + " with type \"" + self.get_type_display() + "\""
def url(self):
if self.type == 5:
return None
what_type = {
0: 'main:post-view',
1: 'main:comment-view',
@@ -1462,11 +1470,13 @@ class Notification(models.Model):
self.source.is_following = False
# In the future, please put giving notifications for classes into their respective classes (right now they're in views)
@staticmethod
def give_notification(user, type, to, post=None, comment=None):
def give_notification(user, type, to, post=None, comment=None, warning=None):
# Just keeping this simple for now, might want to make it better later
# If the user sent a notification to this user at least 5 seconds ago, return False
# Or if user is to
# Or if yeah notifications are off and this is a yeah notification
if type == 5:
return user.notification_sender.create(type=type, to=to, context_warning=warning)
user_is_self_unk = (not type == 3 and user == to)
is_notification_too_fast = (user.notification_sender.filter(created__gt=timezone.now() - timedelta(seconds=5), type=type).exclude(type=4) and not type == 3)
user_no_yeahnotif = (not to.let_yeahnotifs() and (type == 0 or type == 1))
@@ -1606,6 +1616,8 @@ class Conversation(models.Model):
return 3
if len(request.POST['body']) > 50000 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'):
return 1
if request.user.unread_warning():
return 4
upload = None
drawing = None
if request.FILES.get('screen'):
@@ -1771,10 +1783,11 @@ class UserBlock(models.Model):
class AuditLog(models.Model):
id = models.AutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
type = models.SmallIntegerField(choices=((0, "Post delete"), (1, "Comment delete"), (2, "User edit"), (3, "Disable comments"), (4, "User delete"), (5, "Image delete"), (6, "Purge 1"), (7, "Purge 2"), (8, "Purge 3"), (9, "Purge 4"), (10, "Purge 5"), (11, "Un-purge 1"), (12, 'Changed server settings'), ))
type = models.SmallIntegerField(choices=((0, "Post delete"), (1, "Comment delete"), (2, "User edit"), (3, "Disable comments"), (4, "Community edit"), ))
post = models.ForeignKey(Post, related_name='audit_post', null=True, on_delete=models.CASCADE)
comment = models.ForeignKey(Comment, related_name='audit_comment', null=True, on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name='audit_user', null=True, on_delete=models.CASCADE)
community = models.ForeignKey(Community, related_name='audit_community', null=True, on_delete=models.CASCADE)
reasoning = models.TextField(null=True, blank=True, default="")
by = models.ForeignKey(User, related_name='audit_by', on_delete=models.CASCADE)
reversed_by = models.ForeignKey(User, null=True, related_name='audit_reverse_by', on_delete=models.CASCADE)
@@ -4,21 +4,12 @@
<div id="help" class="main-column">
<div class="post-list-outline">
<h2 class="label">Change password</h2>
<form class="setting-form" method="post" action={% url "main:change-password-set" %}>
<form class="setting-form" method="post" action={% url "main:change-password" %}>
<p>You can change your password here</p>
<li class="setting">
<p class="settings-label">Old Password:</p>
<div class="center center-input">
<input type="password" name="old-password" maxlength="100" placeholder="Old Password">
</div>
<p class="settings-label">New Password:</p>
<div class="center center-input">
<input type="password" name="new-password" maxlength="100" placeholder="New Password">
</div>
<p class="settings-label">Confirm Password:</p>
<div class="center center-input">
<input type="password" name="confirm-password" maxlength="100" placeholder="Confirm Password">
</div>
<li class="center-input">
{% for field in form %}
<h3 class='label'>{{ field }}</h3>
{% endfor %}
</li>
{% csrf_token %}
<div class="form-buttons">
@@ -4,15 +4,15 @@
</div>
<div class="community-top-sidebar">
<div class="post-list-outline index-memo">
<h2 class="label">Community creation Q&amp;A</h2>
<h2 class="label">Community creation</h2>
<h2>Why?</h2>
<p>I have no idea, I guess it's cool to have. You can have your own little corner of this site now...</p>
<h2>What can I do with my own community?</h2>
<p>You can edit it, and that's basically it.</p>
<p>Maybe there's a community that's missing which should be here, or you just want a corner of the site to call your own.</p>
<h2>How can I make my own community?</h2>
<p>Assuming you haven't already made one, A button should be visible below that will allow you to create a community. Don't worry, you'll be able to set the icon and banner when editing a community after it has been made. Just keep in mind that creating inappropriate, or shitty communities can result in them getting taken down, if this does happen you will not be refunded your C-Token, and you're shit out of luck.</p>
<h2>What are C-Tokens, and how do I get them?</h2>
<p>Community tokens are a stupid and overcomplicated method of preventing 200,000 communities from being made by one person. You get one when signing up, and you can use it to make your own community. Please do not ask staff for more C-Tokens unless you believe the communities you are going to make are really that good.</p>
<h2>Any future plans?</h2>
<p>no</p>
<p>Community tokens are a stupid and overcomplicated method of preventing a bunch of communities from being made by one person. You get one when signing up, and you can use it to make your own community. Please do not ask staff for more C-Tokens, you probably won't be given any, and let's be honest here, one community is enough for one person.</p>
<h2>Okay great... Now what can I do with my community?</h2>
<p>Whatever you want, really... So long as your community isn't so shitty that we take it down. You are able to edit your community, set your banner, icon and whatnot. And you can ban people from posting and replying in your community if you block someone. Just keep in mind that blocks work both ways, they cannot fuck with you, nor can you fuck with them.</p>
</div>
{% if request.user.c_tokens > 0 %}<a class="big-button" href={% url "main:community-create" %}><span class="symbol-label">Create a community</span></a>{% endif %}
</div>
@@ -7,22 +7,7 @@
<form action="{% url "main:community-search" %}" class="search">
<input maxlength="32" name="query" placeholder="Search all communities" type="text"><input title="Search" type="submit" value="q">
</form>
<!--<div class="close-announce-container">
<div class="close-announce-link">
<a class="title" id="close-text-ann" href="/posts/1186"><b>Closedverse is still in indefinite maintenance.</b></a>
</div>
</div>-->
{% if user.is_warned and user.is_active %}
<div class="notice" style="background-color: #ffc783;border: 1px solid #ffb358;">
<p><b>WARNING</b>: You have been issued a warning by an administrator.
<div>
{% if user.get_warned_reason %}
<p>Reason: "{{ user.get_warned_reason }}"</p>
{% endif %}
</div>
</div>
{% endif %}
{% if not user.is_active and user.is_authenticated %}
<div class="notice" style="background-color: #ff9797;border: 1px solid #ff5252;">
<p><b>Oops</b>: You've been smacked by an admin. Better luck next time.
@@ -44,6 +29,7 @@
{% endif %}
{% if announcements and request.user.is_authenticated %}
<a href="#communities">Skip to communities</a>
<div class="tleft">
<div class="post-list-outline">
<h2 class="label">Latest Announcements</h2>
@@ -20,23 +20,19 @@
{% if request.user.has_freedom %}
<li class="setting-community-icon">
<label class="file-button-container">
<p class="input-label">Community icon: {{ max_icon_size }}MB</p>
<p class="input-label">Community icon:</p>
<span class="button file-upload-button">Upload a new icon</span>
<input accept="image/gif, image/png, image/webp, image/jpeg, image/jpg, image/svg+xml, image/bmp" type="file" style="display: none;" name="community_icon" onchange="loadFile(event)" id="upload-file">
<input accept="image/gif, image/png, image/webp, image/jpeg, image/jpg, image/svg+xml, image/bmp" type="file" style="display: none;" name="community_icon" id="upload-file">
</label>
</li>
<li class="setting-community-banner">
<label class="file-button-container">
<p class="input-label">Community banner: {{ max_banner_size }}MB</p>
<p class="input-label">Community banner:</p>
<span class="button file-upload-button">Upload a new banner</span>
<input accept="image/gif, image/png, image/webp, image/jpeg, image/jpg, image/svg+xml, image/bmp" type="file" style="display: none;" name="community_banner" onchange="loadFile(event)" id="upload-file">
</label>
</li>
<<<<<<< Updated upstream
{% endif %}
=======
{% endif %}
>>>>>>> Stashed changes
<li>
<p>&nbsp;</p>
<input type="checkbox" name="force_login" {% if community.require_auth %}checked{% endif %}>Require login:
@@ -1,16 +1,6 @@
{% extends "closedverse_main/layout.html" %}{% block main-body %}{% load closedverse_community %}
{% community_sidebar community request %}
<div class="main-column">
{% if user.is_warned %}
<div class="notice" style="background-color: #ffc783;border: 1px solid #ffb358;">
<p><b>WARNING</b>: You have been issued a warning by an administrator.
<div>
{% if user.get_warned_reason %}
<p>Reason: "{{ user.get_warned_reason }}"</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="post-list-outline">
<h2 class="label">{{ community.name }}</h2>
{% if request.user.is_authenticated and community.post_perm %}
@@ -0,0 +1 @@
<p>I'm loading it !</p>
@@ -16,7 +16,7 @@
</div>
</label>
<div class="file-button-container">
<span class="input-label">Video <span>MP4s and WEBMs are allowed. (50MB size limit)</span></span>
<span class="input-label">Video <span>MP4s and WEBMs are allowed.</span></span>
<input type="file" class="file-button" name="video" accept="video/mp4, video/webm">
</div>
@@ -8,7 +8,7 @@
{% if message.drawing %}
<p class="post-content-memo"><img src="{{ message.drawing }}"></p>
{% else %}
<div class="post-content-text">{{ message.body|markdown }}</div>
<div class="post-content-text">{{ message.body }}</div>
{% endif %}
{% if message.screenshot %}
<div class="screenshot-container still-image"><img src="{{ message.screenshot }}"></div>
@@ -1,5 +1,5 @@
{% load closedverse_tags %}<div class="news-list-content trigger{% if not notification.read %} notify{% endif %}" tabindex="0" id="{{ notification.unique_id }}" data-href="{{ notification.url }}">
{% user_icon_container notification.source %}
{% load closedverse_tags %}<div class="news-list-content trigger{% if not notification.read %} notify{% endif %}{% if notification.type == 5 %} bigger{% endif %}" tabindex="0" id="{{ notification.unique_id }}" {% if notification.url %}data-href="{{ notification.url }}"{% endif %}>
{% if not notification.type == 5 %}{% user_icon_container notification.source %}{% endif %}
<div class="body">
{% if notification.type == 0 %}
{% print_names notification.all_users %} gave <a href="{{ notification.url }}" class="link">your post&nbsp;({{ notification.context_post.trun|truncatechars:30 }})</a> a Yeah.
@@ -11,6 +11,10 @@
{% print_names notification.all_users %} commented on <a href="{{ notification.url }}" class="link">{{ notification.source.nickname }}'s post&nbsp;({{ notification.context_post.trun|truncatechars:30 }})</a>.
{% elif notification.type == 4 %}
Followed by {% print_names notification.all_users %}.
{% elif notification.type == 5 %}
<p class='warning-notif'>You've received a warning from an administrator!</p>
{% if notification.context_warning.reason %}<p class='warning-reason'><b>Reason: </b>{{ notification.context_warning.reason }}</p>{% endif %}
<p class='warning-note'>To avoid seeing this in the future, it's a good idea to catch up on the rules. Continuous violations of our rules may result in your account getting restricted.</p>
{% endif %}
<span class="timestamp"> {% time notification.latest %}</span>
{% if notification.type == 4 and not notification.source.is_following and not notification.all_users|length > 1 %}
@@ -12,17 +12,6 @@
</div>
{% endif %}
{% if user.is_warned and user.is_active %}
<div class="notice" style="background-color: #ffc783;border: 1px solid #ffb358;">
<p><b>Notice</b>: This user has received a warning by an administrator.</p>
<div>
{% if user.get_warned_reason %}
<p>Reason: "{{ user.get_warned_reason }}"</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="sidebar-container">
{% if profile.favorite.screenshot %}
<a href="{% url "main:post-view" profile.favorite_id %}" id="sidebar-cover" style="background-image:url({{ profile.favorite.screenshot }})">
@@ -50,13 +39,10 @@
<button type="button" data-action="{% url "main:user-fr-delete" user.username %}" data-screen-name="{{ user.nickname }}" class="friend-button unf delete button symbol">Friends</button>
{% endif %}
{% endif %}
{% if not has_authority and request.user.can_manage %}
<button class="button" data-href="{% url "main:user-tools" user.username %}">User Settings</button>
{% endif %}
{% if not general and not user.is_me and profile.can_block %}
{% if not user.is_me %}
<div class="report-buttons-content">
<!-- for later use <button type="button" class="report-button report-user" data-track-label="user" data-track-action="openReportModal" data-track-category="reportViolator" data-modal-open="#report-violator-page">Report Violation</button> -->
<button type="button" class="report-button block-button">{% if profile.is_blocked %}Unblock{% else %}Block{% endif %}</button>
{% if profile.can_block %}<button type="button" class="report-button block-button">{% if profile.is_blocked %}Unblock{% else %}Block{% endif %}</button>{% endif %}
</div>
{% endif %}
</div>
@@ -124,6 +110,21 @@
{% endif %}
</div>
</div>
{% if not has_authority and request.user.can_manage %}
<div class="sidebar-setting sidebar-container">
<div class="sidebar-post-menu">
<a href={% url "main:user-tools" user.username %} class="sidebar-admin-settings symbol{% if selection == 7 %} selected{% endif %}">
<span>User Settings</span>
</a>
<a href={% url "main:user-tools-warnings" user.username %} class="sidebar-admin-warnings symbol{% if selection == 8 %} selected{% endif %}">
<span>Warn User</span>
</a>
<a href={% url "main:user-tools-bans" user.username %} class="sidebar-admin-bans symbol{% if selection == 9 %} selected{% endif %}">
<span>Ban User</span>
</a>
</div>
</div>
{% endif %}
<div class="sidebar-container sidebar-profile">
{% if profile.comment %}
@@ -9,7 +9,6 @@
<h3>General account information:</h3>
<p>IP address: <span>{% if user.addr %}{{ user.addr }}{% else %}Data missing{% endif %}
<p>Signup IP address: <span>{% if user.signup_addr %}{{ user.signup_addr }}{% else %}Data missing{% endif %}</p>
<p>User agent: <span>{% if user.user_agent %}{{ user.user_agent }}{% else %}Data missing{% endif %}
<p>Rank: <span>{% if user.staff %}You are a staff member.{% elif not user.level <= 0 %}You are a moderator. (Level {{ user.level }}){% else %}You are a regular user.{% endif %}
<h3>My content:</h3>
<p>Your account has existed for {{ age }} days! During that time, we've collected:</p>
@@ -7,12 +7,18 @@
<p class="lh">Log In</p>
<p>Sign in to access this instance of {{ brand_name }}</p>
<div class="login-box">
<h3 class="label"><input type="text" class="auth-input" name="username" maxlength="32" placeholder="Username"></h3>
<h3 class="label"><input type="password" class="auth-input" name="password" maxlength="32" placeholder="Password"></h3>
<h3 class='label'>{{ form.username }}</h3>
<h3 class='label'>{{ form.password }}</h3>
</div>
{% csrf_token %}
<p class="red" style="margin-bottom:6px"></p>
<p class="red" style="margin-bottom:6px">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}<br>
{% endfor %}
{% endfor %}
</p>
<button type="submit" class="black-button">Sign In</button>
<div class="ll">
@@ -0,0 +1,25 @@
{% extends "closedverse_main/layout.html" %}
{% block main-body %}{% load closedverse_user %}{% load closedverse_tags %}
{% user_sidebar request user profile 9 False %}
<div class="main-column"><div class="post-list-outline">
<h2 class="label">Ban {{ user.username }}</h2>
<form class="setting-form" method="post" action={% url "main:user-tools-bans" user.username %}>
{% if not banned %}
<li class="setting">
<p class="settings-label">Reasoning:</p>
{{ form.reason }}
<p class="settings-label">Expiry:</p>
{{ form.expiry_date }}
<p class='note'>{{ user.username }} will be banned from {{ brand_name }} until the time is right.</p>
</li>
{% csrf_token %}
<div class="form-buttons">
<input type="submit" class="button apply-button" value="Ban {{ user.username }}">
</div>
{% else %}
<p class="label">This user is banned already.</p>
<p>The ban will expire on: {{ active_ban.expiry_date }}</p>
<p>Time remaining: {{ active_ban.expiry_date|timeuntil }}</p>
{% endif %}
</form></div></div>
{% endblock %}
@@ -0,0 +1,49 @@
{% extends "closedverse_main/layout.html" %}
{% block main-body %}{% load closedverse_user %}{% load closedverse_tags %}
{% user_sidebar request user profile 8 False %}
<div class="main-column"><div class="post-list-outline">
<h2 class="label">Send a warning to {{ user.username }}</h2>
<form class="setting-form" method="post" action={% url "main:user-tools-warnings" user.username %}>
<li class="setting">
<p class="settings-label">Reasoning:</p>
{{ form.reason }}
<p class='note'>Warnings will show as notifications. {{ user.username }} will not be able to post, comment, or message another user until this notification is seen. {{ user.username }} will not see who sent this warning.</p>
</li>
<div class="user-data">
{% if unread_warnings %}<li>
<p class='setting'>Warnings that have not been read yet:<p>
<table style="width:100%">
<tr>
<th>Time</th>
<th>Incident</th>
</tr>
{% for Notifications in unread_warnings %}
<tr>
<td>{{ Notifications.created }}</td>
<td>{{ Notifications.context_warning.reason }}</td>
</tr>
{% endfor %}
</table>{% endif %}
</li>{% if all_warnings %}
<p class='setting'>Incident history:<p>
<table style="width:100%">
<tr>
<th>Time</th>
<th>Issued by</th>
<th>Incident</th>
</tr>
{% for Warning in all_warnings %}
<tr>
<td>{{ Warning.created }}</td>
<td>{{ Warning.by }}</td>
<td>{{ Warning.reason }}</td>
</tr>
{% endfor %}
</table>{% endif %}
</div>
{% csrf_token %}
<div class="form-buttons">
<input type="submit" class="button apply-button" value="Send warning to {{ user.username }}">
</div>
</form></div></div>
{% endblock %}
@@ -1,12 +1,9 @@
{% extends "closedverse_main/layout.html" %}
{% block main-body %}{% load closedverse_user %}{% load closedverse_tags %}
{% user_sidebar request user profile 7 False %}
<div class="main-column"><div class="post-list-outline">
<h2 class="label">Change settings for {{ user.username }}</h2>
<form class="setting-form" method="post" action={% url "main:user-tools-set" user.username %}>
<p>Yes, this form is now ugly, but it's way more powerful.</p>
<li class="setting">
{% user_sidebar_info user %}
{% for field in user_form %}
<li class='setting'>
{% if field.field.widget.input_type == 'checkbox' %}
@@ -38,8 +35,6 @@
<p class='note'>{{ field.help_text }}</p>
</li>
{% endfor %}
</li>
<li class="setting">
<p class="settings-label">Purge content:</p>
{% for field in purge_form %}
@@ -51,7 +46,6 @@
<div class="center center-input">
<p class="settings-label">Metadata:</p>
<div class="user-data">
<p>User agent: <span>{% if user.user_agent %}{{ user.user_agent }}{% else %}Data missing{% endif %}
<p>Rank: <span>{% if user.staff %}Staff member.{% elif not user.level <= 0 %}Moderator. (Level {{ user.level }}){% else %}Regular user.{% endif %}
<p>&nbsp;</p>
</div>
@@ -10,7 +10,6 @@
<h3>General account information:</h3>
<p>IP address: <span>{% if user.addr %}{{ user.addr }}{% else %}Data missing{% endif %}
<p>Signup IP address: <span>{% if user.signup_addr %}{{ user.signup_addr }}{% else %}Data missing{% endif %}</p>
<p>User agent: <span>{% if user.user_agent %}{{ user.user_agent }}{% else %}Data missing{% endif %}
<p>Email: <span>{% if user.email %}{{ user.email }}{% else %}Data missing{% endif %}
<p>Rank: <span>{% if user.staff %}Staff member.{% elif not user.level <= 0 %}Moderator. (Level {{ user.level }}){% else %}Regular user.{% endif %}
</div>
@@ -78,7 +78,7 @@
<li class="setting-website">
<p class="settings-label">Discord Username</p>
<div class="center center-input">
<input type="text" name="external" maxlength="255" placeholder="Discord Tag" value="{{ profile.external }}">
<input type="text" name="external" maxlength="255" placeholder="@" value="{{ profile.external }}">
</div>
<p class="note">Actually, you don't have to put a Discord username here, you can put anything here, such as your Threads username or X username. Discord sure is popular though.</p>
</li>
@@ -0,0 +1 @@
+2 -1
View File
@@ -41,6 +41,8 @@ urlpatterns = [
url(r'users/'+ username +'/tools/set$', views.user_tools_set, name='user-tools-set'),
url(r'users/'+ username +'/tools$', views.user_tools, name='user-tools'),
url(r'users/'+ username +'/tools/meta$', views.user_tools_meta, name='user-tools-meta'),
url(r'users/'+ username +'/tools/warn$', views.user_tools_warnings, name='user-tools-warnings'),
url(r'users/'+ username +'/tools/ban$', views.user_tools_bams, name='user-tools-bans'),
url(r'users/'+ username +'/block$', views.user_addblock, name='user-addblock'),
url(r'users/'+ username +'/block_rm$', views.user_rmblock, name='user-rmblock'),
url(r'my_blacklist$', views.user_blocklist, name='block-list'),
@@ -100,7 +102,6 @@ urlpatterns = [
url(r'invite/create$', views.create_invite, name='create-invite'),
url(r'mydata$', views.my_data, name='my-data'),
url(r'changepassword$', views.change_password, name='change-password'),
url(r'changepassword/set$', views.change_password_set, name='change-password-set'),
url(r'server$', views.server_stat, name='server-stat'),
url(r'help/rules/?$', views.help_rules, name='help-rules'),
url(r'help/faq/?$', views.help_faq, name='help-faq'),
+16 -2
View File
@@ -105,7 +105,10 @@ def video_upload(video):
return settings.MEDIA_URL + fname
ImageFile.LOAD_TRUNCATED_IMAGES = True
def image_upload(img, stream=False, drawing=False, avatar=False):
def image_upload(img, stream=False, drawing=False, avatar=False, icon=False, banner=False):
Imagefield = False
if banner or icon:
Imagefield = True
if stream:
decodedimg = img.read()
else:
@@ -137,7 +140,8 @@ def image_upload(img, stream=False, drawing=False, avatar=False):
}
if orientation in rotations:
im = im.transpose(rotations[orientation])
if avatar:
if avatar or icon:
tiny = True
# crop 1:1
width, height = im.size
min_dimension = min(width, height)
@@ -149,6 +153,7 @@ def image_upload(img, stream=False, drawing=False, avatar=False):
im = im.crop((left, top, right, bottom))
im.thumbnail((256, 256))
else:
tiny = False
im.thumbnail((1280, 1280))
# Let's check the aspect ratio and see if it's crazy
@@ -174,11 +179,20 @@ def image_upload(img, stream=False, drawing=False, avatar=False):
im = im.convert('RGB')
elif 'webp' in img.content_type:
target = 'webp'
# Janky goofy ahh solution to a stupid problem!
if tiny:
floc = imhash + '_tiny' + '.' + target
else:
floc = imhash + '.' + target
# If the file exists, just use it, that's what hashes are for.
if not os.path.exists(settings.MEDIA_ROOT + floc):
im.save(settings.MEDIA_ROOT + floc, target, optimize=True)
if not Imagefield:
return settings.MEDIA_URL + floc
# Not compatible with imagefield otherwise, if this is not here, Images uploaded will not show due to an incorrect URL.
# yes it's a crack den solution but it works.
else: return floc
# Todo: Put this into post/comment delete thingy method
def image_rm(image_url):
+109 -183
View File
@@ -13,6 +13,7 @@ from django.contrib.auth import update_session_auth_hash
from django.db.models import Q, Count, Exists, OuterRef
from django.db.models.functions import Now
from .models import *
from .forms import *
from .util import *
from closedverse import settings
import re
@@ -183,69 +184,33 @@ def community_favorites(request):
})
def login_page(request):
"""Login page! using our own user objects."""
# Redirect the user to / if they're logged in, forcing them to log out
location = '/'
# rn the password form does not take into account if you have the old password format or not.
if request.user.is_authenticated:
return redirect(location)
return redirect('/')
if request.method == 'POST':
# If we don't have all of the POST parameters we want..
if not (request.POST['username'] and request.POST['password']):
# then return that.
return HttpResponseBadRequest("You didn't fill in all of the fields.")
# Now let's authenticate.
# Wait, first check if the user exists. Remove spaces from the username, because some people do that.
# Hold up, first we need to check proxe.
# Uncomment this if you want users to be forbidden from logging in with a proxy.
#if settings.CLOSEDVERSE_PROD and settings.DISALLOW_PROXY and iphub(request.META['REMOTE_ADDR']):
# return HttpResponseNotFound("The user doesn't exist.")
user = User.objects.authenticate(username=request.POST['username'], password=request.POST['password'])
# None = doesn't exist, False = invalid password.
if user is None:
return HttpResponseNotFound("The user doesn't exist.")
else:
# Todo: I might want to do some things relating to making models take care of object stuff like this instead of the view, it's totally messed up now.
successful = False if user[1] is False or not user[0].is_active() else True
LoginAttempt.objects.create(user=user[0], success=successful, user_agent=request.META.get('HTTP_USER_AGENT'), addr=request.META.get('REMOTE_ADDR'))
if user[1] == False:
# check if a hasher could not be identified due to moving from passlib
try:
# will throw a ValueError if no hasher is found
identify_hasher(user[0].password)
except ValueError as e:
# error says "Unknown password hashing algorithm ''......", meaning that the password is not converted
if '\'\'' in str(e):
return HttpResponseBadRequest("The password is either encoded in an unsupported format, or the passwords haven't been updated yet. As part of a recent update, passwords need to be converted - sorry about that. If password resets work, you can use that to make your account usable immediately.")
else:
return HttpResponseBadRequest("The password is either encoded in an unsupported format, or the settings haven't been updated yet. Please add - or direct the server owner to add - hashers_passlib.bcrypt_sha256 to settings.PASSWORD_HASHERS, and then install django-hashers-passlib. I'm sorry for the inconvenience! If password resets work, you can use that immediately.")
else:
return HttpResponse("Invalid password.", status=401)
elif user[1] == 2:
return HttpResponse("This account's password needs to be reset. Contact an admin or reset by email.", status=400)
elif not user[0].is_active():
return HttpResponseForbidden("This account was disabled.")
request.session['passwd'] = user[0].password
login(request, user[0])
# Then, let's get the referrer and either return that or the root.
# Actually, let's not for now.
#if request.META['HTTP_REFERER'] and "login" not in request.META['HTTP_REFERER'] and request.META['HTTP_HOST'] in request.META['HTTP_REFERER']:
# location = request.META['HTTP_REFERER']
#else:
if request.GET.get('next'):
location = request.GET['next']
incorrect_password = False
form = LoginForm(request.POST)
if form.is_valid():
user = User.objects.get(username__iexact=form.cleaned_data['username'])
# no longer logs failed logins
# do I really want to fix that?
LoginAttempt.objects.create(user=user,success=True, user_agent=request.META.get('HTTP_USER_AGENT'),addr=request.META.get('REMOTE_ADDR'))
request.session['passwd'] = user.password
login(request, user)
location = request.GET.get('next', '/')
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
# because if you respond to an ajax with a redirect, the response will just be the page, you can't get the redirect value from js
return HttpResponse(location)
return redirect(location)
else:
form = LoginForm()
return render(request, 'closedverse_main/login_page.html', {
'title': 'Log in',
'form': form,
'allow_signups': settings.allow_signups,
'reset_supported': hasattr(settings, 'DEFAULT_FROM_EMAIL'),
#'classes': ['no-login-btn']
})
def signup_page(request):
"""Signup page, lots of checks here"""
# Redirect the user to / if they're logged in, forcing them to log out
@@ -280,7 +245,6 @@ def signup_page(request):
if not re.compile(r'^[A-Za-z0-9-._]{1,32}$').match(request.POST['username']) or not re.compile(r'[A-Za-z0-9]').match(request.POST['username']):
return HttpResponseBadRequest("Your username either contains invalid characters or is too long (only letters + numbers, dashes, dots and underscores are allowed")
# forbidden keywords
groups = [
[
@@ -305,15 +269,6 @@ def signup_page(request):
if keyword in request.POST['username'].lower() or keyword in request.POST['nickname'].lower():
# perhaps warn admins at a later time
return HttpResponseForbidden(messages[id])
# this is such a miniscule amount of NNIDs that either an admin or a person could
# claim all of them, in which case they could not be reused
#for keyword in ['windowscj', 'gabalt']:
# if keyword in request.POST['origin_id'].lower():
# return HttpResponseForbidden("You're very funny. Unfortunately your funniness blah blah blah fuck off.")
# end of forbidden keywords
conflicting_user = User.objects.filter(Q(username__iexact=request.POST['username']) | Q(username__iexact=request.POST['username'].replace(' ', '')))
if conflicting_user:
return HttpResponseBadRequest("A user with that username already exists.")
@@ -369,7 +324,7 @@ def signup_page(request):
nick = request.POST['nickname']
mii = None
gravatar = True
make = User.objects.closed_create_user(username=request.POST['username'], password=request.POST['password'], email=request.POST.get('email'), addr=request.META['REMOTE_ADDR'], user_agent=request.META['HTTP_USER_AGENT'], signup_addr=request.META['REMOTE_ADDR'], nick=nick, nn=mii, gravatar=gravatar)
make = User.objects.closed_create_user(username=request.POST['username'], password=request.POST['password'], email=request.POST.get('email'), addr=request.META['REMOTE_ADDR'], signup_addr=request.META['REMOTE_ADDR'], nick=nick, nn=mii, gravatar=gravatar)
if invited == True:
invite.used = True
invite.used_by = make
@@ -400,19 +355,11 @@ def forgot_passwd(request):
try:
user = User.objects.get(email=request.POST['email'])
except (User.DoesNotExist, ValueError):
<<<<<<< Updated upstream
return HttpResponseNotFound("There isn't a user with that email address.")
try:
user.password_reset_email(request)
except:
return HttpResponseBadRequest("There was an error submitting that.")
=======
return HttpResponseNotFound("The email address could not be found.")
try:
user.password_reset_email(request)
except Exception as error:
return HttpResponseBadRequest("There was an error submitting that, sorry! Here's why: " + str(error))
>>>>>>> Stashed changes
return HttpResponse("Success! Check your emails, it should have been sent from \"{0}\".".format(settings.DEFAULT_FROM_EMAIL))
if request.GET.get('token'):
user = User.get_from_passwd(request.GET['token'])
@@ -488,7 +435,7 @@ def user_view(request, username):
nickname_old = user.nickname
if profile.cannot_edit:
return json_response("Not allowed.")
if request.POST.get('screen_name') is None or len(request.POST['screen_name']) > 32:
if len(request.POST.get('screen_name')) is 0 or len(request.POST['screen_name']) > 32:
return json_response('Nickname is too long or too short (length '+str(len(request.POST.get('screen_name')))+', max 32)')
if len(request.POST.get('profile_comment')) > 2200:
return json_response('Profile comment is too long (length '+str(len(request.POST.get('profile_comment')))+', max 2200)')
@@ -1044,6 +991,7 @@ def community_favorite_rm(request, community):
def community_tools(request, community):
the_community = get_object_or_404(Community, id=community)
activity_feed = True if the_community.type == 4 else False
if not request.user.is_authenticated:
raise Http404()
can_edit = the_community.can_edit_community(request)
@@ -1052,77 +1000,17 @@ def community_tools(request, community):
return render(request, 'closedverse_main/community_tools.html', {
'title': 'Community tools',
'community': the_community,
'max_icon_size': settings.max_icon_size,
'max_banner_size': settings.max_banner_size,
'activity_feed': activity_feed,
})
def community_tools_set(request, community):
if request.method == 'POST':
the_community = get_object_or_404(Community, id=community)
#do the checks
if not request.user.is_authenticated:
return HttpResponseForbidden()
can_edit = the_community.can_edit_community(request)
if not can_edit:
return HttpResponseForbidden()
<<<<<<< Updated upstream
if len(request.POST.get('community_name')) == 0 or len(request.POST.get('community_name')) >= 100:
return json_response('Your community name is either too short or too long.')
if len(request.POST.get('community_description')) >= 1024:
return json_response('Your community description is too long.')
if int(request.POST.get('community_platform')) >= 8:
return json_response('Invalid Platform type.')
if the_community.is_rm:
return json_response('Community is removed')
if the_community.type == 4:
return json_response('Community is removed')
#do the settings
the_community.name = request.POST.get('community_name')
the_community.description = request.POST.get('community_description')
the_community.platform = (request.POST.get('community_platform') or 0)
the_community.require_auth = False if request.POST.get('force_login') is None else True
ico = request.FILES.get('community_icon')
banner = request.FILES.get('community_banner')
#if icon is not set, ignore it
if ico != None:
#make sure it's an image.
if 'image' in ico.content_type:
# Set the upload limit in megabytes
upload_limit = settings.max_icon_size
max_size = upload_limit * 1024 * 1024
current_size = ico.size
max_mb = max_size / 1024 / 1024
if current_size > max_size:
return json_response('Your icon is too big! Please upload an image under ' + str(max_mb) + 'MB. (' + str(round(current_size / 1024 / 1024, 3)) + 'MB)')
the_community.ico = ico
else:
return json_response('bad image')
if banner != None:
if 'image' in banner.content_type:
# Set the upload limit in megabytes
upload_limit = settings.max_banner_size
max_size = upload_limit * 1024 * 1024
current_size = banner.size
max_mb = max_size / 1024 / 1024
if current_size > max_size:
# i hate this fucking shit
return json_response('Your banner is too big! Please upload an image under ' + str(max_mb) + ' MB. (' + str(round(current_size / 1024 / 1024, 3)) + 'MB)')
the_community.banner = banner
else:
return json_response('bad image')
'''
if request.FILES.get('community_icon') != None:
if
the_community.ico = request.FILES.get('community_icon')
if request.FILES.get('community_banner') != None:
the_community.banner = request.FILES.get('community_banner')
'''
the_community.save()
#AuditLog.objects.create(type=2, user=user, by=request.user, reasoning=request.POST)
=======
form = CommunitySettingForm(request.POST, request.FILES, request , instance=the_community)
if not form.is_valid():
return json_response(form.errors.as_text())
@@ -1139,7 +1027,6 @@ def community_tools_set(request, community):
community.banner = upload
community.save()
AuditLog.objects.create(type=4, community=community, user=community.creator, by=request.user)
>>>>>>> Stashed changes
return HttpResponse()
else:
raise Http404()
@@ -1202,6 +1089,7 @@ def post_create(request, community):
10: "No mr white, you can't make a post entirely consistant of spaces",
11: "The video you've uploaded is invalid.",
12: "Please don't post Zalgo text.",
13: "Please check your notifications.",
}.get(new_post))
# Render correctly whether we're posting to Activity Feed
if community.is_activity():
@@ -1335,6 +1223,7 @@ def post_comments(request, post):
3: "You're making comments too fast, wait a few seconds and try again.",
6: "Not allowed.",
12: "Please don't post Zalgo text.",
13: "Please check your notifications.",
}.get(new_post))
# Give the notification!
if post.is_mine(request.user):
@@ -1670,6 +1559,7 @@ def messages_view(request, username):
1: "Your message is too long ("+str(len(request.POST['body']))+" characters, 2200 max).",
2: "The image you've uploaded is invalid.",
3: "Sorry, but you're sending messages too fast.",
4: "Please check your notifications.",
6: "Not allowed.",
}.get(new_post))
friendship.update()
@@ -1742,18 +1632,19 @@ def prefs(request):
arr = [profile.let_yeahnotifs, lights, request.user.hide_online]
return JsonResponse(arr, safe=False)
@login_required
def user_tools(request, username):
if not request.user.is_authenticated:
raise Http404()
if not request.user.can_manage():
return HttpResponseForbidden()
user = get_object_or_404(User, username__iexact=username)
profile = user.profile()
profile.setup(request)
# check if the requesting user is allowed to change someone
if user.has_authority(request.user):
return HttpResponseForbidden()
user_form = UserForm(instance=user)
profile_form = ProfileForm(instance=profile)
user_form = User_tools_Form(instance=user)
profile_form = Profile_tools_Form(instance=profile)
purge_form = PurgeForm()
seen_by = MetaViews.objects.filter(target_user=user).distinct().order_by('-id')[:10]
has_seen = MetaViews.objects.filter(from_user=user).distinct().order_by('-id')[:10]
@@ -1774,18 +1665,15 @@ def user_tools(request, username):
'accountmatch': accountmatch,
'min_lvl_metadata_perms': settings.min_lvl_metadata_perms,
})
@login_required
def user_tools_meta(request, username):
if not request.user.is_authenticated:
raise Http404()
if not request.user.can_manage():
return HttpResponseForbidden()
if request.user.level < settings.min_lvl_metadata_perms:
if request.user.level < settings.min_lvl_metadata_perms or settings.min_lvl_metadata_perms == 0:
return HttpResponseForbidden()
user = get_object_or_404(User, username__iexact=username)
profile = user.profile()
if user.protect_data:
return HttpResponseForbidden()
profile.setup(request)
# check if the requesting user is allowed to view someone
if user.has_authority(request.user):
return HttpResponseForbidden()
@@ -1822,6 +1710,68 @@ def user_tools_meta(request, username):
'min_lvl_metadata_perms': settings.min_lvl_metadata_perms,
})
@login_required
def user_tools_warnings(request, username):
user = get_object_or_404(User, username__iexact=username)
profile = user.profile()
profile.setup(request)
if not request.user.can_manage():
return HttpResponseForbidden()
if user.has_authority(request.user):
return HttpResponseForbidden()
if request.method == 'POST':
form = Give_warning_form(request.POST)
if form.is_valid():
warning = form.save(commit=False)
warning.to = user
warning.by = request.user
warning.save()
return redirect('main:user-view', user)
unread_warnings = Notification.objects.filter(type=5, to=user, read=False)[:3]
all_warnings = Warning.objects.filter(to=user)[:8]
form = Give_warning_form()
return render(request, 'closedverse_main/man/manage_warnings.html', {
'user': user,
'unread_warnings': unread_warnings,
'all_warnings': all_warnings,
'profile': profile,
'form': form,
})
@login_required
def user_tools_bams(request, username):
user = get_object_or_404(User, username__iexact=username)
profile = user.profile()
profile.setup(request)
active_bans = user.banned_user.filter(active=True).exclude(expiry_date__lte=timezone.now())
if active_bans:
banned = True
active_ban = active_bans.first()
else:
banned = False
active_ban = None
if not request.user.can_manage():
return HttpResponseForbidden()
if user.has_authority(request.user):
return HttpResponseForbidden()
if request.method == 'POST':
form = Give_Ban_Form(request.POST)
if form.is_valid():
ban = form.save(commit=False)
ban.to = user
ban.by = request.user
ban.save()
return redirect('main:user-view', user)
if not banned:
form = Give_Ban_Form()
else: form = None
return render(request, 'closedverse_main/man/manage_bans.html', {
'user': user,
'banned': banned,
'active_ban': active_ban,
'profile': profile,
'form': form,
})
def user_tools_set(request, username):
if request.method == 'POST':
@@ -1835,8 +1785,8 @@ def user_tools_set(request, username):
if user.has_authority(request.user):
return HttpResponseForbidden()
user_form = UserForm(request.POST, instance=user)
profile_form = ProfileForm(request.POST, instance=profile)
user_form = User_tools_Form(request.POST, instance=user)
profile_form = Profile_tools_Form(request.POST, instance=profile)
purge_form = PurgeForm(request.POST)
if purge_form.is_valid():
@@ -1967,50 +1917,26 @@ def my_data(request):
})
@login_required
def change_password(request):
if not request.user.is_authenticated:
raise Http404()
user = request.user
return render(request, 'closedverse_main/change-password.html', {
'user': user,
'title': 'Change Password',
})
def change_password_set(request):
if request.method == 'POST':
if not request.user.is_authenticated:
raise Http404()
user = request.user
old = request.POST.get('old-password')
new = request.POST.get('new-password')
confirm = request.POST.get('confirm-password')
# make sure they are filled out
# a bit inefficient but who cares?
if not old:
return json_response('Please specify your old password.')
if not new:
return json_response('Please specify your new password.')
if not confirm:
return json_response('Please specify your new password again.')
if not new == confirm:
return json_response('Passwords do not match')
if old == new:
return json_response('The old password and new password can\'t be the same.')
form = Settomgs_Change_Password(request.POST)
if form.is_valid():
old = form.cleaned_data.get('Old_Password')
new = form.cleaned_data.get('New_Password')
if not user.check_password(old):
return json_response('The old password specified does not match the user\'s password. Enter the password you use as of right now.')
# do the length check
#if len(new) < settings.minimum_password_length:
# return json_response('The new password must be at least ' + str(settings.minimum_password_length) + ' characters long.')
try:
validate_password(new, user=user)
except ValidationError as error:
return json_response(error)
# do the thing
return json_response('The old password specified does not match the user\'s password. Enter the password you use as of right now.', "Invalid old password")
user.set_password(new)
user.save()
update_session_auth_hash(request, user)
return json_response("Success! Now you can log in with your new password!")
return json_response("Success! Now you can log in with your new password!", "Finished")
return json_response(form.errors.as_text())
else:
raise Http404
form = Settomgs_Change_Password(request.POST)
return render(request, 'closedverse_main/change-password.html', {
'user': user,
'form': form,
'title': 'Change Password',
})
def whatads(request):
return render(request, 'closedverse_main/help/whatads.html', {'title': 'What are user-generated ads?'})
def help_rules(request):
+21
View File
@@ -144,6 +144,12 @@ input.disabled {
.green {
color: green !important;
}
.warning-reason {
margin-top: 6px;
}
.warning-note {
margin-top: 6px;
}
body {
font-size: 14px;
text-align: center;
@@ -1943,6 +1949,9 @@ font-size: 18px;
.button-wiiu:hover {
background: #1088b6;
}
.list .bigger {
padding: 29px;
}
.list > li,
.list > div,
.list > a,
@@ -3386,6 +3395,18 @@ border-top: 1px solid var(--theme, #94ff3d);
content: "e";
color: var(--theme, #5ac800);
}
.sidebar-setting .sidebar-admin-settings:before {
content: "y";
color: var(--theme, #5ac800);
}
.sidebar-setting .sidebar-admin-warnings:before {
content: "n";
color: var(--theme, #5ac800);
}
.sidebar-setting .sidebar-admin-bans:before {
content: "x";
color: var(--theme, #5ac800);
}
.sidebar-setting .sidebar-menu-replies:before {
content: "r";
color: var(--theme, #5ac800);
+4 -2
View File
@@ -2021,7 +2021,9 @@ var Olv = Olv || {};
event.preventDefault();
});
c.on("drop paste", handleFileChange);
c.on("drop", handleFileChange);
// upon choosing an image, and pasting plain text after, no image would be uploaded for some reason.
// yeah i got no fucking clue how to fix this shit.
c.on("olv:entryform:post:done", function() {
imageDimensions.text(b.EntryForm.tempPollutionButImageFormAllowedText);
@@ -3221,7 +3223,7 @@ mode_post = 0;
b.router.connect("^/users/[^\/]+/favorites$", function(a, c, d) {
b.Content.autopagerize(".community-list", d)
}),
b.router.connect("^/login/$|^/signup/$", function(c, d, e) {
b.router.connect("^/signup/$", function(c, d, e) {
function lfinish(b) {
window.location.href=b
//a('body').attr('sess-usern', a('input[name=username]').val())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

+76 -62
View File
@@ -4,37 +4,37 @@
<svg
version="1.1"
id="svg1"
width="262.66666"
height="64"
viewBox="0 0 262.66666 64"
width="270.35233"
height="61.079422"
viewBox="0 0 270.35233 61.079422"
sodipodi:docname="menu-logo.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xml:space="preserve"
inkscape:export-filename="menu-logo.png"
inkscape:export-xdpi="170.44423"
inkscape:export-ydpi="170.44423"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1"><linearGradient
id="linearGradient3"
inkscape:collect="always"><stop
style="stop-color:#fe3939;stop-opacity:1;"
offset="0"
id="stop3" /><stop
style="stop-color:#c72a2a;stop-opacity:1;"
offset="1"
id="stop4" /></linearGradient><linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
inkscape:collect="always"><stop
style="stop-color:#ff3a3a;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
id="stop1" /><stop
style="stop-color:#ad1a1a;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<rect
x="72.546936"
y="26.759483"
width="260.0376"
height="68.768394"
id="rect55" />
<linearGradient
id="stop2" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
@@ -42,9 +42,31 @@
y1="36.274925"
x2="136.69847"
y2="70.450951"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient4"
x1="149.797"
y1="0"
x2="149.797"
y2="325.388"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient5"
gradientUnits="userSpaceOnUse"
x1="149.797"
y1="0"
x2="149.797"
y2="325.388" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient6"
gradientUnits="userSpaceOnUse"
x1="149.797"
y1="0"
x2="149.797"
y2="325.388" /></defs><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
@@ -61,38 +83,33 @@
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="g1" />
<g
inkscape:current-layer="g1" /><g
inkscape:groupmode="layer"
inkscape:label="Image"
id="g1">
<g
id="g56">
<rect
id="g1"
transform="translate(-0.78294009,-1.5827909)"><g
id="g56"><rect
style="fill:#ff4d4d;fill-opacity:1"
id="rect53"
width="5.9472828"
height="5.9472828"
x="8.667901"
y="16.853588"
transform="rotate(-45)" />
<rect
transform="rotate(-45)" /><rect
style="fill:#ad1a1a;fill-opacity:1"
id="rect53-3"
width="5.9472828"
height="7.947773"
x="8.667901"
y="22.800871"
transform="rotate(-45)" />
<rect
transform="rotate(-45)" /><rect
style="fill:#ff7676;fill-opacity:1"
id="rect53-3-6"
width="5.9472828"
height="7.947773"
x="16.853588"
y="-8.667901"
transform="rotate(45)" />
<rect
transform="rotate(45)" /><rect
style="fill:#ff7676;fill-opacity:1"
id="rect53-3-6-5"
width="7.9418755"
@@ -100,8 +117,7 @@
x="22.803822"
y="-0.72012806"
transform="rotate(45)"
ry="0" />
<rect
ry="0" /><rect
style="fill:#ff7676;fill-opacity:1"
id="rect53-3-6-5-8"
width="13.31431"
@@ -109,8 +125,7 @@
x="30.748648"
y="12.59123"
transform="rotate(45)"
ry="0" />
<rect
ry="0" /><rect
style="fill:#ad1a1a;fill-opacity:1"
id="rect53-3-6-5-8-6"
width="13.31431"
@@ -118,8 +133,7 @@
x="-12.591232"
y="44.062958"
transform="rotate(-45)"
ry="0" />
<rect
ry="0" /><rect
style="fill:#ad1a1a;fill-opacity:1"
id="rect53-3-6-5-9"
width="7.9418755"
@@ -127,44 +141,44 @@
x="0.72307706"
y="30.748646"
transform="rotate(-45)"
ry="0" />
<path
ry="0" /><path
id="rect54"
style="fill:#ff4d4d;fill-opacity:1"
transform="rotate(-45)"
d="m 0.72012806,22.800871 7.94777294,0 -7.94777294,7.947775 z"
sodipodi:nodetypes="cccc" />
<path
d="M 0.72012806,22.800871 H 8.667901 l -7.94777294,7.947775 z"
sodipodi:nodetypes="cccc" /><path
id="rect54-2"
style="fill:#d13434;fill-opacity:1"
d="m 27.871707,15.613444 -5.619924,5.619924 -2e-6,-11.2398501 z"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc" /><path
id="rect54-29"
style="fill:#ff4d4d;stroke-width:1.67523;fill-opacity:1"
style="fill:#ff4d4d;fill-opacity:1;stroke-width:1.67523"
d="m 12.839233,30.645922 9.414637,-9.414637 3e-6,18.829278 z"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc" /><path
id="rect54-2-3"
style="fill:#d13434;stroke-width:1.67523;fill-opacity:1"
style="fill:#d13434;fill-opacity:1;stroke-width:1.67523"
d="m 31.668508,30.645922 -9.414637,9.414639 -3e-6,-18.829278 z"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc" /><path
style="fill:#b68147;fill-opacity:1"
d="m 22.253868,40.060561 -6.028143,6.028146 v 12.674516 l 6.028143,3.898989 z"
id="path55"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc" /><path
style="fill:#5e4125;fill-opacity:1"
d="m 22.253873,40.060562 6.028143,6.028146 v 12.674516 l -6.028143,3.898989 z"
id="path55-2"
sodipodi:nodetypes="ccccc" />
</g>
<path
sodipodi:nodetypes="ccccc" /></g><path
style="font-weight:bold;font-size:42.6667px;font-family:Comfortaa;-inkscape-font-specification:'Comfortaa Bold';white-space:pre;shape-padding:4.21878;fill:url(#linearGradient2)"
d="m 95.965639,70.450952 q -3.456002,0 -6.485338,-1.280001 -2.986669,-1.322668 -5.248004,-3.62667 -2.218668,-2.346668 -3.498669,-5.461337 -1.237334,-3.114669 -1.237334,-6.741338 0,-3.584003 1.237334,-6.656005 1.280001,-3.114669 3.498669,-5.418671 2.261335,-2.346669 5.248004,-3.669336 2.986669,-1.322668 6.485338,-1.322668 3.285336,0 5.632001,0.853334 2.38934,0.853334 4.77867,2.816002 0.34134,0.256 0.512,0.554667 0.21334,0.256 0.256,0.554667 0.0853,0.256001 0.0853,0.640001 0,0.810667 -0.59734,1.365334 -0.55466,0.512001 -1.36533,0.597334 -0.81067,0.04267 -1.57867,-0.554667 -1.57867,-1.365334 -3.28533,-2.090668 -1.664005,-0.725334 -4.437341,-0.725334 -2.517335,0 -4.736003,1.024001 -2.218669,1.024 -3.925336,2.816002 -1.664002,1.792001 -2.602669,4.181336 -0.938667,2.346669 -0.938667,5.034671 0,2.730668 0.938667,5.120003 0.938667,2.346669 2.602669,4.13867 1.706667,1.792001 3.925336,2.816002 2.218668,0.981334 4.736003,0.981334 2.176002,0 4.096001,-0.725334 1.96267,-0.768 3.75467,-2.090668 0.768,-0.554667 1.49334,-0.469334 0.768,0.04267 1.28,0.597334 0.512,0.512001 0.512,1.450668 0,0.426667 -0.17067,0.853334 -0.17067,0.384 -0.512,0.725334 -2.304,1.834668 -4.94934,2.773335 -2.602665,0.938668 -5.504001,0.938668 z M 123.0163,70.194951 q -3.54133,0 -6.31467,-1.493334 -2.73067,-1.536001 -4.30933,-4.181336 -1.536,-2.688002 -1.536,-6.144005 0,-3.498669 1.45066,-6.144005 1.49334,-2.688002 4.09601,-4.181336 2.60267,-1.536001 5.97333,-1.536001 3.32801,0 5.71734,1.493334 2.38934,1.450668 3.62667,4.053337 1.28,2.560001 1.28,5.930671 0,0.810667 -0.55466,1.365334 -0.55467,0.512 -1.40801,0.512 h -17.19468 v -3.413335 h 17.06668 l -1.74933,1.194667 q -0.0427,-2.133335 -0.85333,-3.797336 -0.81067,-1.706668 -2.30401,-2.688002 -1.49333,-0.981334 -3.62667,-0.981334 -2.432,0 -4.18133,1.066667 -1.70667,1.066668 -2.60267,2.944002 -0.896,1.834669 -0.896,4.181337 0,2.346668 1.06667,4.181336 1.06666,1.834668 2.944,2.901336 1.87733,1.066667 4.30933,1.066667 1.32267,0 2.68801,-0.469333 1.408,-0.512001 2.26133,-1.152001 0.64,-0.469334 1.36534,-0.469334 0.768,-0.04267 1.32266,0.426667 0.72534,0.640001 0.768,1.408001 0.0427,0.768001 -0.68266,1.322668 -1.45067,1.152001 -3.62667,1.877335 -2.13334,0.725333 -4.09601,0.725333 z m 25.25868,0 q -3.328,0 -6.016,-1.536001 -2.64534,-1.578668 -4.22401,-4.26667 -1.536,-2.688002 -1.536,-6.058671 0,-3.370669 1.408,-6.016004 1.45067,-2.688002 3.92534,-4.224003 2.47467,-1.578668 5.58934,-1.578668 2.51733,0 4.65067,1.066667 2.13333,1.024001 3.584,2.816002 V 38.877595 q 0,-0.981334 0.59733,-1.578668 0.64,-0.597334 1.57867,-0.597334 0.98134,0 1.57867,0.597334 0.59733,0.597334 0.59733,1.578668 v 19.456014 q 0,3.370669 -1.57866,6.058671 -1.53601,2.688002 -4.18134,4.26667 -2.64534,1.536001 -5.97334,1.536001 z m 0,-3.840002 q 2.176,0 3.88267,-1.024001 1.70667,-1.066668 2.688,-2.901336 0.98134,-1.834668 0.98134,-4.096003 0,-2.304001 -0.98134,-4.096003 -0.98133,-1.792001 -2.688,-2.816002 -1.70667,-1.066667 -3.88267,-1.066667 -2.13333,0 -3.88267,1.066667 -1.70667,1.024001 -2.73067,2.816002 -0.98133,1.792002 -0.98133,4.096003 0,2.261335 0.98133,4.096003 1.024,1.834668 2.73067,2.901336 1.74934,1.024001 3.88267,1.024001 z m 29.14132,3.840002 q -3.11467,0 -5.58934,-1.536001 -2.47467,-1.578668 -3.92533,-4.224003 -1.40801,-2.688002 -1.40801,-6.058671 0,-3.370669 1.53601,-6.058671 1.57866,-2.688002 4.224,-4.224003 2.688,-1.578668 6.016,-1.578668 3.32801,0 5.97334,1.578668 2.64534,1.536001 4.18134,4.224003 1.57867,2.688002 1.57867,6.058671 h -1.66401 q 0,3.370669 -1.45066,6.058671 -1.408,2.645335 -3.88267,4.224003 -2.47467,1.536001 -5.58934,1.536001 z m 0.85333,-3.840002 q 2.17601,0 3.88267,-1.024001 1.70667,-1.066668 2.68801,-2.858669 0.98133,-1.834668 0.98133,-4.096003 0,-2.304002 -0.98133,-4.096003 -0.98134,-1.834668 -2.68801,-2.858669 -1.70666,-1.066667 -3.88267,-1.066667 -2.13333,0 -3.88267,1.066667 -1.70666,1.024001 -2.73066,2.858669 -0.98134,1.792001 -0.98134,4.096003 0,2.261335 0.98134,4.096003 1.024,1.792001 2.73066,2.858669 1.74934,1.024001 3.88267,1.024001 z m 9.55734,3.712002 q -0.93866,0 -1.57866,-0.597333 -0.59734,-0.640001 -0.59734,-1.578668 v -6.528005 l 0.81067,-4.52267 3.54134,1.536001 v 9.514674 q 0,0.938667 -0.64,1.578668 -0.59734,0.597333 -1.53601,0.597333 z m 10.75199,-14.634677 q 0,-2.560002 1.23733,-4.565337 1.28,-2.048002 3.41334,-3.242669 2.13333,-1.194668 4.69333,-1.194668 2.56,0 3.79734,0.853334 1.28,0.810668 0.98133,1.962668 -0.128,0.597334 -0.512,0.938668 -0.34133,0.298667 -0.81066,0.384 -0.46934,0.08533 -1.02401,-0.04267 -2.73066,-0.554667 -4.90667,-0.08533 -2.176,0.469334 -3.456,1.749335 -1.23733,1.280001 -1.23733,3.242669 z m 0.0427,14.592011 q -1.024,0 -1.57867,-0.512001 -0.55466,-0.554667 -0.55466,-1.621334 V 48.818935 q 0,-1.024 0.55466,-1.578667 0.55467,-0.554667 1.57867,-0.554667 1.06667,0 1.57867,0.554667 0.55467,0.512 0.55467,1.578667 V 67.89095 q 0,1.024 -0.55467,1.578668 -0.512,0.554667 -1.57867,0.554667 z"
id="text55"
transform="matrix(1.468878,0,0,1.468878,-67.310013,-46.383647)"
aria-label="Cedar" />
</g>
</svg>
aria-label="Cedar" /><g
fill="#2ba977"
id="g2"
transform="matrix(0.07550438,0,0,0.07550438,251.86754,5.3031849)"
style="fill:url(#linearGradient4)"><path
d="m 114.784,0 h 53.278 v 244.191 c -27.29,5.162 -47.38,7.193 -69.117,7.193 C 33.873,251.316 0,222.245 0,166.412 0,112.617 35.93,77.704 91.608,77.704 c 8.64,0 15.222,0.68 23.176,2.717 z m 1.867,124.427 c -6.24,-2.038 -11.382,-2.717 -17.965,-2.717 -26.947,0 -42.512,16.437 -42.512,45.243 0,28.046 14.88,43.532 42.17,43.532 5.896,0 10.696,-0.332 18.307,-1.351 z"
id="path1"
style="fill:url(#linearGradient5)" /><path
d="m 255.187,84.26 v 122.263 c 0,42.105 -3.154,62.353 -12.411,79.81 -8.64,16.783 -20.022,27.366 -43.541,39.055 l -49.438,-23.297 c 23.519,-10.93 34.901,-20.588 42.17,-35.327 7.61,-15.072 10.01,-32.529 10.01,-78.445 V 84.261 h 53.21 z M 196.608,0 h 53.278 v 54.135 h -53.278 z"
id="path2"
style="fill:url(#linearGradient6)" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 B