mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 07:26:33 +03:00
added user history
This commit is contained in:
parent
099cca3aab
commit
e3454a142a
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("blog", "0006_post_public"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="comment",
|
||||
options={"ordering": ["-rating", "-created"], "verbose_name": "Comment"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="commentrating",
|
||||
options={"verbose_name": "Comment rating"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="post",
|
||||
options={"ordering": ["-created"], "verbose_name": "Post"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="postrating",
|
||||
options={"verbose_name": "Post rating"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="tag",
|
||||
options={"verbose_name": "Tag"},
|
||||
),
|
||||
]
|
|
@ -5,12 +5,13 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.models import User
|
||||
from akarpov.users.services.history import UserHistoryModel
|
||||
from akarpov.utils.string import cleanhtml
|
||||
|
||||
|
||||
class Post(BaseImageModel, ShortLink):
|
||||
class Post(BaseImageModel, ShortLinkModel, UserHistoryModel):
|
||||
title = models.CharField(max_length=100, blank=False)
|
||||
body = RichTextUploadingField(blank=False)
|
||||
|
||||
|
@ -64,21 +65,28 @@ def get_absolute_url(self):
|
|||
return reverse("blog:post", kwargs={"slug": self.slug})
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Post"
|
||||
ordering = ["-created"]
|
||||
|
||||
class SlugMeta:
|
||||
slug_length = 3
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
class Tag(UserHistoryModel):
|
||||
name = models.CharField(max_length=20, unique=True)
|
||||
color = ColorField(blank=True, default="#FF0000")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("blog:post_list") + f"?tag={self.name}"
|
||||
|
||||
class PostRating(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Tag"
|
||||
|
||||
|
||||
class PostRating(UserHistoryModel):
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="post_ratings"
|
||||
)
|
||||
|
@ -86,6 +94,9 @@ class PostRating(models.Model):
|
|||
|
||||
vote_up = models.BooleanField(blank=False)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.post.get_absolute_url()
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.user}'s vote up on {self.post.title}"
|
||||
|
@ -94,10 +105,11 @@ def __str__(self):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Post rating"
|
||||
unique_together = ["user", "post"]
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
class Comment(UserHistoryModel):
|
||||
parent = models.ForeignKey("self", blank=True, null=True, on_delete=models.CASCADE)
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
|
||||
|
@ -107,14 +119,18 @@ class Comment(models.Model):
|
|||
|
||||
rating = models.IntegerField(default=0)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.post.get_absolute_url() + f"#{self.id}"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.author.username}'s comment on {self.post.title}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Comment"
|
||||
ordering = ["-rating", "-created"]
|
||||
|
||||
|
||||
class CommentRating(models.Model):
|
||||
class CommentRating(UserHistoryModel):
|
||||
comment = models.ForeignKey(
|
||||
Comment, on_delete=models.CASCADE, related_name="ratings"
|
||||
)
|
||||
|
@ -127,5 +143,9 @@ class CommentRating(models.Model):
|
|||
def __str__(self):
|
||||
return f"{self.user}'s vote up" if self.vote_up else f"{self.user}'s vote down"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.comment.get_absolute_url()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Comment rating"
|
||||
unique_together = ["comment", "user"]
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("files", "0023_rename_download_basefileitem_downloads_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="file",
|
||||
options={"verbose_name": "File"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="filereport",
|
||||
options={"verbose_name": "File report"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="folder",
|
||||
options={"verbose_name": "Folder"},
|
||||
),
|
||||
]
|
|
@ -19,7 +19,8 @@
|
|||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from akarpov.files.services.files import trash_file_upload, user_unique_file_upload
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.services.history import UserHistoryModel
|
||||
|
||||
|
||||
class BaseFileItem(PolymorphicModel):
|
||||
|
@ -59,7 +60,7 @@ def save(self, *args, **kwargs):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class File(BaseFileItem, TimeStampedModel, ShortLink):
|
||||
class File(BaseFileItem, TimeStampedModel, ShortLinkModel, UserHistoryModel):
|
||||
"""model to store user's files"""
|
||||
|
||||
private = BooleanField(default=True)
|
||||
|
@ -102,8 +103,11 @@ def get_absolute_url(self):
|
|||
def __str__(self):
|
||||
return f"file: {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "File"
|
||||
|
||||
class Folder(BaseFileItem, ShortLink):
|
||||
|
||||
class Folder(BaseFileItem, ShortLinkModel, UserHistoryModel):
|
||||
name = CharField(max_length=100)
|
||||
slug = SlugField(max_length=20, blank=True)
|
||||
private = BooleanField(default=True)
|
||||
|
@ -118,6 +122,9 @@ def get_absolute_url(self):
|
|||
def __str__(self):
|
||||
return f"folder: {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Folder"
|
||||
|
||||
|
||||
class FileInTrash(TimeStampedModel):
|
||||
name = CharField(max_length=200, blank=True)
|
||||
|
@ -132,5 +139,11 @@ class FileReport(Model):
|
|||
created = DateTimeField(auto_now_add=True)
|
||||
file = ForeignKey("files.File", related_name="reports", on_delete=CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "File report"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.file.get_absolute_url()
|
||||
|
||||
def __str__(self):
|
||||
return f"report on {self.file}"
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("gallery", "0002_image_extra_data_image_image_city_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="collection",
|
||||
options={"verbose_name": "Collection"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="image",
|
||||
options={"verbose_name": "Image"},
|
||||
),
|
||||
]
|
|
@ -4,11 +4,12 @@
|
|||
from location_field.models.plain import PlainLocationField
|
||||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.services.history import UserHistoryModel
|
||||
from akarpov.utils.files import user_file_upload_mixin
|
||||
|
||||
|
||||
class Collection(TimeStampedModel, ShortLink):
|
||||
class Collection(TimeStampedModel, ShortLinkModel, UserHistoryModel):
|
||||
name = models.CharField(max_length=250, blank=True)
|
||||
description = models.TextField()
|
||||
public = models.BooleanField(default=False)
|
||||
|
@ -22,8 +23,11 @@ def get_absolute_url(self):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Collection"
|
||||
|
||||
class Image(TimeStampedModel, ShortLink, BaseImageModel):
|
||||
|
||||
class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel):
|
||||
collection = models.ForeignKey(
|
||||
"Collection", related_name="images", on_delete=models.CASCADE
|
||||
)
|
||||
|
@ -46,8 +50,11 @@ def get_absolute_url(self):
|
|||
def __str__(self):
|
||||
return self.image.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Image"
|
||||
|
||||
class Tag(ShortLink):
|
||||
|
||||
class Tag(ShortLinkModel):
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
class MusicConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
verbose_name = "Music"
|
||||
name = "akarpov.music"
|
||||
|
||||
def ready(self):
|
||||
|
|
17
akarpov/music/migrations/0002_alter_playlist_options.py
Normal file
17
akarpov/music/migrations/0002_alter_playlist_options.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("music", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="playlist",
|
||||
options={"verbose_name": "Playlist"},
|
||||
),
|
||||
]
|
|
@ -2,10 +2,11 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.services.history import UserHistoryModel
|
||||
|
||||
|
||||
class Author(BaseImageModel, ShortLink):
|
||||
class Author(BaseImageModel, ShortLinkModel):
|
||||
name = models.CharField(max_length=200)
|
||||
link = models.URLField(blank=True)
|
||||
|
||||
|
@ -16,7 +17,7 @@ def __str__(self):
|
|||
return self.name
|
||||
|
||||
|
||||
class Album(BaseImageModel, ShortLink):
|
||||
class Album(BaseImageModel, ShortLinkModel):
|
||||
name = models.CharField(max_length=200)
|
||||
link = models.URLField(blank=True)
|
||||
|
||||
|
@ -27,7 +28,7 @@ def __str__(self):
|
|||
return self.name
|
||||
|
||||
|
||||
class Song(BaseImageModel, ShortLink):
|
||||
class Song(BaseImageModel, ShortLinkModel):
|
||||
link = models.URLField(blank=True)
|
||||
length = models.IntegerField(null=True)
|
||||
played = models.IntegerField(default=0)
|
||||
|
@ -50,7 +51,7 @@ class SlugMeta:
|
|||
slug_length = 10
|
||||
|
||||
|
||||
class Playlist(ShortLink):
|
||||
class Playlist(ShortLinkModel, UserHistoryModel):
|
||||
name = models.CharField(max_length=200)
|
||||
private = models.BooleanField(default=False)
|
||||
creator = models.ForeignKey(
|
||||
|
@ -64,6 +65,9 @@ def get_absolute_url(self):
|
|||
def get_songs(self):
|
||||
return self.songs.all().values("song")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Playlist"
|
||||
|
||||
|
||||
class PlaylistSong(models.Model):
|
||||
order = models.IntegerField()
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
|
||||
class PipelinerConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
verbose_name = "Pipeliner"
|
||||
name = "akarpov.pipeliner"
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
<li><a class="dropdown-item {% active_link 'users:update' %}" href="{% url 'users:update' %}">Settings</a></li>
|
||||
<li><a class="dropdown-item {% active_link 'users:detail' request.user.username %}" href="{% url 'users:detail' request.user.username %}">Profile</a></li>
|
||||
<li><a class="dropdown-item {% active_link 'tools:qr:create' %}" href="{% url 'tools:promocodes:activate' %}">Activate promocode</a></li>
|
||||
<li><a class="dropdown-item {% active_link 'tools:qr:create' %}" href="{% url 'users:history' %}">History</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
{% for comment in post.get_comments %}
|
||||
<div class="col mb-5">
|
||||
<div class="d-flex flex-start">
|
||||
<a href="#{{ comment.id }}"></a>
|
||||
{% if comment.author.image_cropped %}
|
||||
<img class="rounded-circle shadow-1-strong me-3"
|
||||
src="{{ comment.author.image_cropped.url }}" alt="avatar" width="65"
|
||||
|
@ -109,7 +110,6 @@
|
|||
|
||||
<script>
|
||||
function addComment(){
|
||||
console.log("aaaaa")
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
28
akarpov/templates/users/history.html
Normal file
28
akarpov/templates/users/history.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<a href="{% url 'users:history_delete' %}" class="btn btn-danger ms-4 mt-3 mb-3">Delete all history</a>
|
||||
<ul style="list-style: none;">
|
||||
{% for record in userhistory_list %}
|
||||
<li class="mt-1 card border-0">
|
||||
<a href="{{ record.get_link }}" class="stretched-link"></a>
|
||||
{% if record.type == 'note' %}
|
||||
<h4 class="me-5"><i class="bi bi-dot fs-6"></i> {{ record.name }}</h4>
|
||||
{% elif record.type == 'create' %}
|
||||
<h4 class="me-5"><i class="bi bi-plus fs-6"></i> {{ record.name }}</h4>
|
||||
{% elif record.type == 'update' %}
|
||||
<h4 class="me-5"><i class="bi bi-pencil fs-6"></i> {{ record.name }}</h4>
|
||||
{% elif record.type == 'delete' %}
|
||||
<h4 class="me-5"><i class="bi bi-trash3 fs-6"></i> {{ record.name }}</h4>
|
||||
{% elif record.type == 'warning' %}
|
||||
<h4 class="text-bg-warning me-5"><i class="bi bi-exclamation-triangle fs-6"></i> {{ record.name }}</h4>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-12">{{ record.description }}</div>
|
||||
<div class="col-auto">{{ record.created | time:"H:i"}} {{ record.created | date:"d.m.Y"}}</div>
|
||||
</div>
|
||||
<hr class="ms-1 me-5">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
17
akarpov/test_platform/migrations/0008_alter_form_options.py
Normal file
17
akarpov/test_platform/migrations/0008_alter_form_options.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("test_platform", "0007_alter_form_short_link"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="form",
|
||||
options={"verbose_name": "Form"},
|
||||
),
|
||||
]
|
|
@ -8,12 +8,13 @@
|
|||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.models import User
|
||||
from akarpov.users.services.history import UserHistoryModel
|
||||
from akarpov.utils.base import SubclassesMixin
|
||||
|
||||
|
||||
class Form(BaseImageModel, ShortLink):
|
||||
class Form(BaseImageModel, ShortLinkModel, UserHistoryModel):
|
||||
name = models.CharField(max_length=200, blank=False)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
@ -45,6 +46,9 @@ def get_absolute_url(self):
|
|||
def __str__(self):
|
||||
return f"form: {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Form"
|
||||
|
||||
|
||||
class BaseQuestion(PolymorphicModel, SubclassesMixin):
|
||||
type = "no_type"
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
|
||||
class PromocodesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
verbose_name = "Promocodes"
|
||||
name = "akarpov.tools.promocodes"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
class Link(TimeStampedModel):
|
||||
source = models.URLField(blank=False)
|
||||
slug = models.SlugField()
|
||||
slug = models.SlugField(db_index=True)
|
||||
creator = models.ForeignKey(
|
||||
"users.User", related_name="links", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
@ -99,7 +99,7 @@ def update_model_link(sender, instance, **kwargs):
|
|||
instance.short_link = None
|
||||
|
||||
|
||||
class ShortLink(SlugModel):
|
||||
class ShortLinkModel(SlugModel):
|
||||
short_link: Link | None = models.ForeignKey(
|
||||
"shortener.Link", blank=True, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
|
67
akarpov/users/migrations/0009_userhistory.py
Normal file
67
akarpov/users/migrations/0009_userhistory.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-01 10:43
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("users", "0008_alter_user_left_file_upload"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserHistory",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("note", "note"),
|
||||
("create", "create"),
|
||||
("update", "update"),
|
||||
("delete", "delete"),
|
||||
("warning", "warning"),
|
||||
]
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=500)),
|
||||
("object_id", models.PositiveIntegerField()),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="history",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["content_type", "object_id"],
|
||||
name="users_userh_content_7e9fa5_idx",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-04 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0009_userhistory"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userhistory",
|
||||
name="description",
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userhistory",
|
||||
name="name",
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-04 09:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0010_userhistory_description_alter_userhistory_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="userhistory",
|
||||
options={"ordering": ["-created"]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userhistory",
|
||||
name="created",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -1,14 +1,16 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db.models import BigIntegerField, CharField, TextField
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLink
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
|
||||
|
||||
class User(AbstractUser, BaseImageModel, ShortLink):
|
||||
class User(AbstractUser, BaseImageModel, ShortLinkModel):
|
||||
"""
|
||||
Default custom user model for akarpov.
|
||||
If adding fields that need to be filled at user signup,
|
||||
|
@ -16,13 +18,13 @@ class User(AbstractUser, BaseImageModel, ShortLink):
|
|||
"""
|
||||
|
||||
#: First and last name do not cover name patterns around the globe
|
||||
name = CharField(_("Name of User"), blank=True, max_length=255)
|
||||
about = TextField(_("Description"), blank=True, max_length=100)
|
||||
name = models.CharField(_("Name of User"), blank=True, max_length=255)
|
||||
about = models.TextField(_("Description"), blank=True, max_length=100)
|
||||
first_name = None # type: ignore
|
||||
last_name = None # type: ignore
|
||||
|
||||
# files
|
||||
left_file_upload = BigIntegerField(
|
||||
left_file_upload = models.BigIntegerField(
|
||||
"Left file upload(in bites)", default=0, validators=[MinValueValidator(0)]
|
||||
)
|
||||
|
||||
|
@ -34,3 +36,33 @@ def get_absolute_url(self):
|
|||
|
||||
"""
|
||||
return reverse("users:detail", kwargs={"username": self.username})
|
||||
|
||||
|
||||
class UserHistory(models.Model):
|
||||
class RecordType(models.TextChoices):
|
||||
note = "note", "note"
|
||||
create = "create", "create"
|
||||
update = "update", "update"
|
||||
delete = "delete", "delete"
|
||||
warning = "warning", "warning"
|
||||
|
||||
type = models.CharField(choices=RecordType.choices)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey("User", related_name="history", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.CharField(max_length=500, blank=True)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
def get_link(self):
|
||||
if hasattr(self.object, "get_absolute_url"):
|
||||
return self.object.get_absolute_url()
|
||||
return ""
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created"]
|
||||
indexes = [models.Index(fields=["content_type", "object_id"])]
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
|
112
akarpov/users/services/history.py
Normal file
112
akarpov/users/services/history.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
from django.db import models
|
||||
from django.db.models import Model
|
||||
|
||||
from akarpov.users.models import User, UserHistory
|
||||
from akarpov.utils.models import get_app_verbose_name, get_object_name, get_object_user
|
||||
|
||||
|
||||
def create_history_note(user: User, name: str, description: str, obj: Model):
|
||||
UserHistory.objects.create(
|
||||
type=UserHistory.RecordType.note,
|
||||
user=user,
|
||||
name=name,
|
||||
description=description,
|
||||
object=obj,
|
||||
)
|
||||
|
||||
|
||||
def create_history_creation_note(user: User, name: str, description: str, obj: Model):
|
||||
UserHistory.objects.create(
|
||||
type=UserHistory.RecordType.create,
|
||||
user=user,
|
||||
name=name,
|
||||
description=description,
|
||||
object=obj,
|
||||
)
|
||||
|
||||
|
||||
def create_history_update_note(user: User, name: str, description: str, obj: Model):
|
||||
UserHistory.objects.create(
|
||||
type=UserHistory.RecordType.update,
|
||||
user=user,
|
||||
name=name,
|
||||
description=description,
|
||||
object=obj,
|
||||
)
|
||||
|
||||
|
||||
def create_history_delete_note(user: User, name: str, description: str, obj: Model):
|
||||
UserHistory.objects.create(
|
||||
type=UserHistory.RecordType.delete,
|
||||
user=user,
|
||||
name=name,
|
||||
description=description,
|
||||
object=obj,
|
||||
)
|
||||
|
||||
|
||||
def create_history_warning_note(user: User, name: str, description: str, obj: Model):
|
||||
# TODO: notify user here
|
||||
UserHistory.objects.create(
|
||||
type=UserHistory.RecordType.warning,
|
||||
user=user,
|
||||
name=name,
|
||||
description=description,
|
||||
object=obj,
|
||||
)
|
||||
|
||||
|
||||
def create_history_creation_note_on_create(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
user = get_object_user(instance)
|
||||
if user:
|
||||
create_history_creation_note(
|
||||
user,
|
||||
get_app_verbose_name(sender._meta.app_label),
|
||||
f"Created {sender._meta.verbose_name.title()} {get_object_name(instance)}",
|
||||
instance,
|
||||
)
|
||||
|
||||
|
||||
def create_history_update_note_on_update(sender, instance, **kwargs):
|
||||
if instance.id:
|
||||
user = get_object_user(instance)
|
||||
if not kwargs["update_fields"]:
|
||||
create_history_update_note(
|
||||
user,
|
||||
get_app_verbose_name(sender._meta.app_label),
|
||||
f"Updated {sender._meta.verbose_name.title()} {get_object_name(instance)}",
|
||||
instance,
|
||||
)
|
||||
|
||||
|
||||
def create_history_delete_note_on_delete(sender, instance, **kwargs):
|
||||
user = get_object_user(instance)
|
||||
create_history_delete_note(
|
||||
user,
|
||||
get_app_verbose_name(sender._meta.app_label),
|
||||
f"Deleted {sender._meta.verbose_name.title()} {get_object_name(instance)}",
|
||||
instance,
|
||||
)
|
||||
|
||||
|
||||
class UserHistoryModel(models.Model):
|
||||
"""
|
||||
creates user history records on model change
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
models.signals.pre_save.connect(
|
||||
create_history_update_note_on_update, sender=cls
|
||||
)
|
||||
models.signals.post_save.connect(
|
||||
create_history_creation_note_on_create, sender=cls
|
||||
)
|
||||
models.signals.post_delete.connect(
|
||||
create_history_delete_note_on_delete, sender=cls
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
|
@ -1,7 +1,20 @@
|
|||
from allauth.account.signals import (
|
||||
email_added,
|
||||
email_changed,
|
||||
email_removed,
|
||||
password_reset,
|
||||
user_logged_in,
|
||||
)
|
||||
from allauth.socialaccount.signals import social_account_added
|
||||
from django.contrib.auth import user_logged_out
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.users.models import User
|
||||
from akarpov.users.services.history import (
|
||||
create_history_note,
|
||||
create_history_warning_note,
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=User)
|
||||
|
@ -9,3 +22,42 @@ def user_create(sender, instance: User, **kwargs):
|
|||
if instance.id is None:
|
||||
# give user some space on file share on register
|
||||
instance.left_file_upload += 100 * 1024 * 1024
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def user_logged_in(request, user, **kwargs):
|
||||
create_history_note(user, "User", "log in", user)
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def user_logged_out(request, user, **kwargs):
|
||||
create_history_note(user, "User", "log out", user)
|
||||
|
||||
|
||||
@receiver(password_reset)
|
||||
def user_password_reset(request, user, **kwargs):
|
||||
create_history_warning_note(user, "User", "password reset", user)
|
||||
|
||||
|
||||
@receiver(email_changed)
|
||||
def user_email_change(request, user, from_email_address, to_email_address, **kwargs):
|
||||
create_history_warning_note(
|
||||
user, "User", f"user email changed to {to_email_address}", user
|
||||
)
|
||||
|
||||
|
||||
@receiver(email_added)
|
||||
def user_email_add(request, user, email_address, **kwargs):
|
||||
create_history_warning_note(user, "User", f"email {email_address} added", user)
|
||||
|
||||
|
||||
@receiver(email_removed)
|
||||
def user_email_remove(request, user, email_address, **kwargs):
|
||||
create_history_warning_note(user, "User", f"email {email_address} removed", user)
|
||||
|
||||
|
||||
@receiver(social_account_added)
|
||||
def user_account_add(request, sociallogin, **kwargs):
|
||||
create_history_warning_note(
|
||||
request.user, "User", f"added {sociallogin.provider} account", request.user
|
||||
)
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
from django.urls import path
|
||||
|
||||
from akarpov.users.views import user_detail_view, user_redirect_view, user_update_view
|
||||
from akarpov.users.views import (
|
||||
user_detail_view,
|
||||
user_history_delete_view,
|
||||
user_history_view,
|
||||
user_redirect_view,
|
||||
user_update_view,
|
||||
)
|
||||
|
||||
app_name = "users"
|
||||
urlpatterns = [
|
||||
path("redirect/", view=user_redirect_view, name="redirect"),
|
||||
path("update/", view=user_update_view, name="update"),
|
||||
path("history/", view=user_history_view, name="history"),
|
||||
path("history/delete", view=user_history_delete_view, name="history_delete"),
|
||||
path("<str:username>/", view=user_detail_view, name="detail"),
|
||||
]
|
||||
|
|
|
@ -3,13 +3,15 @@
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, RedirectView, UpdateView
|
||||
from django.views.generic import DetailView, ListView, RedirectView, UpdateView
|
||||
|
||||
from akarpov.users.models import UserHistory
|
||||
from akarpov.users.services.history import create_history_warning_note
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserDetailView(DetailView):
|
||||
|
||||
model = User
|
||||
slug_field = "username"
|
||||
slug_url_kwarg = "username"
|
||||
|
@ -19,7 +21,6 @@ class UserDetailView(DetailView):
|
|||
|
||||
|
||||
class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
|
||||
model = User
|
||||
fields = ["username", "name", "image", "about"]
|
||||
success_message = _("Information successfully updated")
|
||||
|
@ -38,7 +39,6 @@ def get_object(self):
|
|||
|
||||
|
||||
class UserRedirectView(LoginRequiredMixin, RedirectView):
|
||||
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self):
|
||||
|
@ -46,3 +46,30 @@ def get_redirect_url(self):
|
|||
|
||||
|
||||
user_redirect_view = UserRedirectView.as_view()
|
||||
|
||||
|
||||
class UserHistoryListView(LoginRequiredMixin, ListView):
|
||||
model = UserHistory
|
||||
template_name = "users/history.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return UserHistory.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
user_history_view = UserHistoryListView.as_view()
|
||||
|
||||
|
||||
class UserHistoryDeleteView(LoginRequiredMixin, RedirectView):
|
||||
def get_redirect_url(self):
|
||||
q = UserHistory.objects.filter(user=self.request.user).exclude(
|
||||
type=UserHistory.RecordType.warning
|
||||
)
|
||||
if q:
|
||||
q.delete()
|
||||
create_history_warning_note(
|
||||
self.request.user, "History", "Deleted history", self.request.user
|
||||
)
|
||||
return reverse("users:history")
|
||||
|
||||
|
||||
user_history_delete_view = UserHistoryDeleteView.as_view()
|
||||
|
|
31
akarpov/utils/models.py
Normal file
31
akarpov/utils/models.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from functools import lru_cache
|
||||
|
||||
from django.apps import apps
|
||||
from django.db.models import Model
|
||||
|
||||
from akarpov.users.models import User
|
||||
|
||||
|
||||
def get_object_name(obj: Model) -> str:
|
||||
if hasattr(obj, "title"):
|
||||
return obj.title
|
||||
elif hasattr(obj, "name"):
|
||||
return obj.name
|
||||
elif hasattr(obj, "__str__"):
|
||||
return obj.__str__()
|
||||
return ""
|
||||
|
||||
|
||||
def get_object_user(obj: Model) -> User | None:
|
||||
if hasattr(obj, "creator"):
|
||||
return obj.creator
|
||||
elif hasattr(obj, "user"):
|
||||
return obj.user
|
||||
elif hasattr(obj, "owner"):
|
||||
return obj.owner
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_app_verbose_name(app: str) -> str:
|
||||
return apps.get_app_config(app).verbose_name
|
Loading…
Reference in New Issue
Block a user