added user history

This commit is contained in:
Alexander Karpov 2023-07-04 14:00:05 +03:00
parent 099cca3aab
commit e3454a142a
26 changed files with 605 additions and 37 deletions

View File

@ -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"},
),
]

View File

@ -5,12 +5,13 @@
from django.urls import reverse from django.urls import reverse
from akarpov.common.models import BaseImageModel 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.models import User
from akarpov.users.services.history import UserHistoryModel
from akarpov.utils.string import cleanhtml from akarpov.utils.string import cleanhtml
class Post(BaseImageModel, ShortLink): class Post(BaseImageModel, ShortLinkModel, UserHistoryModel):
title = models.CharField(max_length=100, blank=False) title = models.CharField(max_length=100, blank=False)
body = RichTextUploadingField(blank=False) body = RichTextUploadingField(blank=False)
@ -64,21 +65,28 @@ def get_absolute_url(self):
return reverse("blog:post", kwargs={"slug": self.slug}) return reverse("blog:post", kwargs={"slug": self.slug})
class Meta: class Meta:
verbose_name = "Post"
ordering = ["-created"] ordering = ["-created"]
class SlugMeta: class SlugMeta:
slug_length = 3 slug_length = 3
class Tag(models.Model): class Tag(UserHistoryModel):
name = models.CharField(max_length=20, unique=True) name = models.CharField(max_length=20, unique=True)
color = ColorField(blank=True, default="#FF0000") color = ColorField(blank=True, default="#FF0000")
def __str__(self): def __str__(self):
return self.name 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 = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="post_ratings" User, on_delete=models.CASCADE, related_name="post_ratings"
) )
@ -86,6 +94,9 @@ class PostRating(models.Model):
vote_up = models.BooleanField(blank=False) vote_up = models.BooleanField(blank=False)
def get_absolute_url(self):
return self.post.get_absolute_url()
def __str__(self): def __str__(self):
return ( return (
f"{self.user}'s vote up on {self.post.title}" f"{self.user}'s vote up on {self.post.title}"
@ -94,10 +105,11 @@ def __str__(self):
) )
class Meta: class Meta:
verbose_name = "Post rating"
unique_together = ["user", "post"] unique_together = ["user", "post"]
class Comment(models.Model): class Comment(UserHistoryModel):
parent = models.ForeignKey("self", blank=True, null=True, on_delete=models.CASCADE) parent = models.ForeignKey("self", blank=True, null=True, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments") post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(User, 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) rating = models.IntegerField(default=0)
def get_absolute_url(self):
return self.post.get_absolute_url() + f"#{self.id}"
def __str__(self): def __str__(self):
return f"{self.author.username}'s comment on {self.post.title}" return f"{self.author.username}'s comment on {self.post.title}"
class Meta: class Meta:
verbose_name = "Comment"
ordering = ["-rating", "-created"] ordering = ["-rating", "-created"]
class CommentRating(models.Model): class CommentRating(UserHistoryModel):
comment = models.ForeignKey( comment = models.ForeignKey(
Comment, on_delete=models.CASCADE, related_name="ratings" Comment, on_delete=models.CASCADE, related_name="ratings"
) )
@ -127,5 +143,9 @@ class CommentRating(models.Model):
def __str__(self): def __str__(self):
return f"{self.user}'s vote up" if self.vote_up else f"{self.user}'s vote down" 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: class Meta:
verbose_name = "Comment rating"
unique_together = ["comment", "user"] unique_together = ["comment", "user"]

View File

@ -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"},
),
]

View File

@ -19,7 +19,8 @@
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from akarpov.files.services.files import trash_file_upload, user_unique_file_upload 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): class BaseFileItem(PolymorphicModel):
@ -59,7 +60,7 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class File(BaseFileItem, TimeStampedModel, ShortLink): class File(BaseFileItem, TimeStampedModel, ShortLinkModel, UserHistoryModel):
"""model to store user's files""" """model to store user's files"""
private = BooleanField(default=True) private = BooleanField(default=True)
@ -102,8 +103,11 @@ def get_absolute_url(self):
def __str__(self): def __str__(self):
return f"file: {self.name}" return f"file: {self.name}"
class Meta:
verbose_name = "File"
class Folder(BaseFileItem, ShortLink):
class Folder(BaseFileItem, ShortLinkModel, UserHistoryModel):
name = CharField(max_length=100) name = CharField(max_length=100)
slug = SlugField(max_length=20, blank=True) slug = SlugField(max_length=20, blank=True)
private = BooleanField(default=True) private = BooleanField(default=True)
@ -118,6 +122,9 @@ def get_absolute_url(self):
def __str__(self): def __str__(self):
return f"folder: {self.name}" return f"folder: {self.name}"
class Meta:
verbose_name = "Folder"
class FileInTrash(TimeStampedModel): class FileInTrash(TimeStampedModel):
name = CharField(max_length=200, blank=True) name = CharField(max_length=200, blank=True)
@ -132,5 +139,11 @@ class FileReport(Model):
created = DateTimeField(auto_now_add=True) created = DateTimeField(auto_now_add=True)
file = ForeignKey("files.File", related_name="reports", on_delete=CASCADE) 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): def __str__(self):
return f"report on {self.file}" return f"report on {self.file}"

View 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"},
),
]

View File

@ -4,11 +4,12 @@
from location_field.models.plain import PlainLocationField from location_field.models.plain import PlainLocationField
from akarpov.common.models import BaseImageModel 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 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) name = models.CharField(max_length=250, blank=True)
description = models.TextField() description = models.TextField()
public = models.BooleanField(default=False) public = models.BooleanField(default=False)
@ -22,8 +23,11 @@ def get_absolute_url(self):
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = "Collection"
class Image(TimeStampedModel, ShortLink, BaseImageModel):
class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel):
collection = models.ForeignKey( collection = models.ForeignKey(
"Collection", related_name="images", on_delete=models.CASCADE "Collection", related_name="images", on_delete=models.CASCADE
) )
@ -46,8 +50,11 @@ def get_absolute_url(self):
def __str__(self): def __str__(self):
return self.image.name 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) name = models.CharField(max_length=255, null=True, blank=True)
def get_absolute_url(self): def get_absolute_url(self):

View File

@ -2,7 +2,7 @@
class MusicConfig(AppConfig): class MusicConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" verbose_name = "Music"
name = "akarpov.music" name = "akarpov.music"
def ready(self): def ready(self):

View 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"},
),
]

View File

@ -2,10 +2,11 @@
from django.urls import reverse from django.urls import reverse
from akarpov.common.models import BaseImageModel 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) name = models.CharField(max_length=200)
link = models.URLField(blank=True) link = models.URLField(blank=True)
@ -16,7 +17,7 @@ def __str__(self):
return self.name return self.name
class Album(BaseImageModel, ShortLink): class Album(BaseImageModel, ShortLinkModel):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
link = models.URLField(blank=True) link = models.URLField(blank=True)
@ -27,7 +28,7 @@ def __str__(self):
return self.name return self.name
class Song(BaseImageModel, ShortLink): class Song(BaseImageModel, ShortLinkModel):
link = models.URLField(blank=True) link = models.URLField(blank=True)
length = models.IntegerField(null=True) length = models.IntegerField(null=True)
played = models.IntegerField(default=0) played = models.IntegerField(default=0)
@ -50,7 +51,7 @@ class SlugMeta:
slug_length = 10 slug_length = 10
class Playlist(ShortLink): class Playlist(ShortLinkModel, UserHistoryModel):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
private = models.BooleanField(default=False) private = models.BooleanField(default=False)
creator = models.ForeignKey( creator = models.ForeignKey(
@ -64,6 +65,9 @@ def get_absolute_url(self):
def get_songs(self): def get_songs(self):
return self.songs.all().values("song") return self.songs.all().values("song")
class Meta:
verbose_name = "Playlist"
class PlaylistSong(models.Model): class PlaylistSong(models.Model):
order = models.IntegerField() order = models.IntegerField()

View File

@ -2,5 +2,5 @@
class PipelinerConfig(AppConfig): class PipelinerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" verbose_name = "Pipeliner"
name = "akarpov.pipeliner" name = "akarpov.pipeliner"

View File

@ -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: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 '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 '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> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>

View File

@ -83,6 +83,7 @@
{% for comment in post.get_comments %} {% for comment in post.get_comments %}
<div class="col mb-5"> <div class="col mb-5">
<div class="d-flex flex-start"> <div class="d-flex flex-start">
<a href="#{{ comment.id }}"></a>
{% if comment.author.image_cropped %} {% if comment.author.image_cropped %}
<img class="rounded-circle shadow-1-strong me-3" <img class="rounded-circle shadow-1-strong me-3"
src="{{ comment.author.image_cropped.url }}" alt="avatar" width="65" src="{{ comment.author.image_cropped.url }}" alt="avatar" width="65"
@ -109,7 +110,6 @@
<script> <script>
function addComment(){ function addComment(){
console.log("aaaaa")
} }
</script> </script>
{% endblock %} {% endblock %}

View 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 %}

View 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"},
),
]

View File

@ -8,12 +8,13 @@
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from akarpov.common.models import BaseImageModel 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.models import User
from akarpov.users.services.history import UserHistoryModel
from akarpov.utils.base import SubclassesMixin from akarpov.utils.base import SubclassesMixin
class Form(BaseImageModel, ShortLink): class Form(BaseImageModel, ShortLinkModel, UserHistoryModel):
name = models.CharField(max_length=200, blank=False) name = models.CharField(max_length=200, blank=False)
description = models.TextField(blank=True) description = models.TextField(blank=True)
@ -45,6 +46,9 @@ def get_absolute_url(self):
def __str__(self): def __str__(self):
return f"form: {self.name}" return f"form: {self.name}"
class Meta:
verbose_name = "Form"
class BaseQuestion(PolymorphicModel, SubclassesMixin): class BaseQuestion(PolymorphicModel, SubclassesMixin):
type = "no_type" type = "no_type"

View File

@ -2,5 +2,5 @@
class PromocodesConfig(AppConfig): class PromocodesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" verbose_name = "Promocodes"
name = "akarpov.tools.promocodes" name = "akarpov.tools.promocodes"

View File

@ -9,7 +9,7 @@
class Link(TimeStampedModel): class Link(TimeStampedModel):
source = models.URLField(blank=False) source = models.URLField(blank=False)
slug = models.SlugField() slug = models.SlugField(db_index=True)
creator = models.ForeignKey( creator = models.ForeignKey(
"users.User", related_name="links", null=True, on_delete=models.SET_NULL "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 instance.short_link = None
class ShortLink(SlugModel): class ShortLinkModel(SlugModel):
short_link: Link | None = models.ForeignKey( short_link: Link | None = models.ForeignKey(
"shortener.Link", blank=True, null=True, on_delete=models.SET_NULL "shortener.Link", blank=True, null=True, on_delete=models.SET_NULL
) )

View 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",
)
],
},
),
]

View File

@ -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),
),
]

View File

@ -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,
),
]

View File

@ -1,14 +1,16 @@
from django.contrib.auth.models import AbstractUser 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.core.validators import MinValueValidator
from django.db.models import BigIntegerField, CharField, TextField from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from akarpov.common.models import BaseImageModel 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. Default custom user model for akarpov.
If adding fields that need to be filled at user signup, 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 #: First and last name do not cover name patterns around the globe
name = CharField(_("Name of User"), blank=True, max_length=255) name = models.CharField(_("Name of User"), blank=True, max_length=255)
about = TextField(_("Description"), blank=True, max_length=100) about = models.TextField(_("Description"), blank=True, max_length=100)
first_name = None # type: ignore first_name = None # type: ignore
last_name = None # type: ignore last_name = None # type: ignore
# files # files
left_file_upload = BigIntegerField( left_file_upload = models.BigIntegerField(
"Left file upload(in bites)", default=0, validators=[MinValueValidator(0)] "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}) 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

View 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

View File

@ -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.db.models.signals import pre_save
from django.dispatch import receiver from django.dispatch import receiver
from akarpov.users.models import User from akarpov.users.models import User
from akarpov.users.services.history import (
create_history_note,
create_history_warning_note,
)
@receiver(pre_save, sender=User) @receiver(pre_save, sender=User)
@ -9,3 +22,42 @@ def user_create(sender, instance: User, **kwargs):
if instance.id is None: if instance.id is None:
# give user some space on file share on register # give user some space on file share on register
instance.left_file_upload += 100 * 1024 * 1024 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
)

View File

@ -1,10 +1,18 @@
from django.urls import path 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" app_name = "users"
urlpatterns = [ urlpatterns = [
path("redirect/", view=user_redirect_view, name="redirect"), path("redirect/", view=user_redirect_view, name="redirect"),
path("update/", view=user_update_view, name="update"), 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"), path("<str:username>/", view=user_detail_view, name="detail"),
] ]

View File

@ -3,13 +3,15 @@
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ 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() User = get_user_model()
class UserDetailView(DetailView): class UserDetailView(DetailView):
model = User model = User
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
@ -19,7 +21,6 @@ class UserDetailView(DetailView):
class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = User model = User
fields = ["username", "name", "image", "about"] fields = ["username", "name", "image", "about"]
success_message = _("Information successfully updated") success_message = _("Information successfully updated")
@ -38,7 +39,6 @@ def get_object(self):
class UserRedirectView(LoginRequiredMixin, RedirectView): class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False permanent = False
def get_redirect_url(self): def get_redirect_url(self):
@ -46,3 +46,30 @@ def get_redirect_url(self):
user_redirect_view = UserRedirectView.as_view() 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
View 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