Skip to content

Commit 4c0c2c2

Browse files
authored
Merge pull request #496 from PROCOLLAB-github/dev
Dev
2 parents 4989763 + 36c01d8 commit 4c0c2c2

18 files changed

Lines changed: 680 additions & 54 deletions

core/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import os
2+
import logging
3+
import pandas as pd
4+
15
from django.core.mail import EmailMultiAlternatives
26

37

8+
logger = logging.getLogger()
9+
10+
411
class Email:
512
"""
613
Send email messages
@@ -24,3 +31,36 @@ def get_user_online_cache_key(user) -> str:
2431

2532
def get_users_online_cache_key() -> str:
2633
return "online_users"
34+
35+
36+
class XlsxFileToExport:
37+
"""
38+
Writing data to `xlsx` file.
39+
`filename` must contain `.xlsx` format prefix.
40+
All data on 1 page.
41+
"""
42+
43+
def __init__(self, filename="output.xlsx"):
44+
self.filename = filename
45+
46+
def write_data_to_xlsx(self, data: list[dict], sheet_name: str = "scores") -> None:
47+
try:
48+
data_frames = pd.DataFrame(data)
49+
with pd.ExcelWriter(self.filename) as writer:
50+
data_frames.to_excel(writer, sheet_name=sheet_name, index=False)
51+
except Exception as e:
52+
logger.error(f"Write export rates data error: {str(e)}", exc_info=True)
53+
raise
54+
55+
def get_binary_data_from_self_file(self) -> bytes:
56+
try:
57+
with open(self.filename, "rb") as f:
58+
binary_data = f.read()
59+
return binary_data
60+
except Exception as e:
61+
logger.error(f"Read export rates data error: {str(e)}", exc_info=True)
62+
raise
63+
64+
def delete_self_xlsx_file_from_local_machine(self) -> None:
65+
if os.path.isfile(self.filename) and self.filename.endswith(".xlsx"):
66+
os.remove(self.filename)

news/filters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class NewsFilter(filters.FilterSet):
1818
.filter(datetime_created__gt=datetime.datetime(...))
1919
"""
2020

21-
# title__contains = filters.Filter(field_name="title", lookup_expr="contains")
2221
text__contains = filters.Filter(field_name="text", lookup_expr="contains")
2322
datetime_created__gt = filters.DateTimeFilter(
2423
field_name="datetime_created", lookup_expr="gt"

partner_programs/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
from django.utils import timezone
99

1010
from mailing.views import MailingTemplateRender
11+
from core.utils import XlsxFileToExport
1112
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
1213
from project_rates.models import Criteria, ProjectScore
1314
from projects.models import Project
14-
from partner_programs.services import XlsxFileToExport, ProjectScoreDataPreparer
15+
from partner_programs.services import ProjectScoreDataPreparer
1516

1617

1718
@admin.register(PartnerProgram)

partner_programs/services.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import logging
2-
import os
3-
import pandas as pd
42

53
from partner_programs.models import PartnerProgramUserProfile
64
from project_rates.models import Criteria, ProjectScore
@@ -9,39 +7,6 @@
97
logger = logging.getLogger()
108

119

12-
class XlsxFileToExport:
13-
"""
14-
Writing data to `xlsx` file.
15-
`filename` must contain `.xlsx` format prefix.
16-
All data on 1 page.
17-
"""
18-
19-
def __init__(self, filename="output.xlsx"):
20-
self.filename = filename
21-
22-
def write_data_to_xlsx(self, data: list[dict], sheet_name="scores") -> None:
23-
try:
24-
data_frames = pd.DataFrame(data)
25-
with pd.ExcelWriter(self.filename) as writer:
26-
data_frames.to_excel(writer, sheet_name=sheet_name, index=False)
27-
except Exception as e:
28-
logger.error(f"Write export rates data error: {str(e)}", exc_info=True)
29-
raise
30-
31-
def get_binary_data_from_self_file(self) -> bytes:
32-
try:
33-
with open(self.filename, "rb") as f:
34-
binary_data = f.read()
35-
return binary_data
36-
except Exception as e:
37-
logger.error(f"Read export rates data error: {str(e)}", exc_info=True)
38-
raise
39-
40-
def delete_self_xlsx_file_from_local_machine(self) -> None:
41-
if os.path.isfile(self.filename) and self.filename.endswith(".xlsx"):
42-
os.remove(self.filename)
43-
44-
4510
class ProjectScoreDataPreparer:
4611
"""
4712
Data preparer about project_rates by experts.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.11 on 2024-12-02 11:40
2+
3+
from django.db import migrations, models
4+
import projects.models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("projects", "0022_alter_project_options_project_is_company"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="project",
16+
name="cover_image_address",
17+
field=models.URLField(
18+
blank=True,
19+
default=projects.models.DefaultProjectCover.get_random_file_link,
20+
help_text="If left blank, will set a link to the image from the 'Обложки проектов'",
21+
null=True,
22+
),
23+
),
24+
]

projects/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def get_random_file(cls):
5050
# FIXME: this is not efficient, but for ~10 default covers it should be ok
5151
return cls.objects.order_by("?").first().image
5252

53+
@classmethod
54+
def get_random_file_link(cls):
55+
# FIXME: this is not efficient, but for ~10 default covers it should be ok
56+
return cls.objects.order_by("?").first().image.link if cls.objects.order_by("?").first().image else None
57+
5358
class Meta:
5459
verbose_name = "Обложка проекта"
5560
verbose_name_plural = "Обложки проектов"
@@ -71,6 +76,7 @@ class Project(models.Model):
7176
leader: A ForeignKey referring to the User model.
7277
draft: A boolean indicating if Project is a draft.
7378
is_company: A boolean indicating if Project is a company.
79+
cover_image_address: A URLField cover image URL address.
7480
cover: A ForeignKey referring to the UserFile model, which is the image cover of the project.
7581
datetime_created: A DateTimeField indicating date of creation.
7682
datetime_updated: A DateTimeField indicating date of update.
@@ -101,6 +107,14 @@ class Project(models.Model):
101107
draft = models.BooleanField(blank=False, default=True)
102108
is_company = models.BooleanField(null=False, default=False)
103109

110+
cover_image_address = models.URLField(
111+
null=True,
112+
blank=True,
113+
default=DefaultProjectCover.get_random_file_link,
114+
help_text="If left blank, will set a link to the image from the 'Обложки проектов'",
115+
)
116+
117+
# TODO DELETE (deprecated field) after full migrate `cover_image_address`.
104118
cover = models.ForeignKey(
105119
UserFile,
106120
default=DefaultProjectCover.get_random_file,
@@ -136,6 +150,12 @@ def get_collaborators_user_list(self) -> list[User]:
136150
def __str__(self):
137151
return f"Project<{self.id}> - {self.name}"
138152

153+
def save(self, *args, **kwargs):
154+
"""Set random cover image if `cover_image_address` blank."""
155+
if self.cover_image_address is None:
156+
self.cover_image_address = DefaultProjectCover.get_random_file_link()
157+
super().save(*args, **kwargs)
158+
139159
class Meta:
140160
ordering = ["-is_company", "-hidden_score", "-datetime_created"]
141161
verbose_name = "Проект"

projects/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class Meta:
128128
"datetime_updated",
129129
"views_count",
130130
"cover",
131+
"cover_image_address",
131132
"partner_programs_tags",
132133
]
133134
read_only_fields = [

templates/users/admin/users_change_list.html

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@
33

44
{% block object-tools-items %}
55
{{ block.super }}
6-
<a href="#" class="addlink" previewlistener="true" onclick="get_users()">
7-
Выгрузка ФИО|Email всех пользователей
8-
</a>
6+
<li>
7+
<a href="#" class="addlink" previewlistener="true" onclick="get_users()">
8+
Выгрузка ФИО|Email всех пользователей
9+
</a>
10+
</li>
11+
<li>
12+
<a href="#" class="addlink" previewlistener="true" onclick="get_users_activity()">
13+
Выгрузка активности
14+
</a>
15+
</li>
916

1017
<script>
1118
function get_users() {
1219
window.open("{% url 'admin:users_email_excel' %}", '_blank').focus();
1320
}
21+
22+
function get_users_activity() {
23+
window.open("{% url 'admin:users_activity_excel' %}", '_blank').focus();
24+
}
1425
</script>
15-
{% endblock %}
26+
{% endblock %}

users/admin.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from datetime import date
22

33
import tablib
4+
import urllib.parse
45
from django.conf import settings
56
from django.contrib import admin
67
from django.contrib.auth.models import Permission
78
from django.http import HttpResponse
89
from django.shortcuts import redirect
910
from django.urls import path
1011

12+
from core.utils import XlsxFileToExport
1113
from mailing.views import MailingTemplateRender
14+
from users.services.users_activity import UserActivityDataPreparer
1215
from .helpers import send_verification_completed_email, force_verify_user
1316
from .models import (
1417
CustomUser,
@@ -227,6 +230,11 @@ def get_urls(self):
227230
self.admin_site.admin_view(self.all_users_email_excel),
228231
name="users_email_excel",
229232
),
233+
path(
234+
"users-activity-excel/",
235+
self.admin_site.admin_view(self.get_users_activity),
236+
name="users_activity_excel",
237+
),
230238
]
231239
return custom_urls + default_urls
232240

@@ -248,6 +256,22 @@ def force_verify(self, request, object_id):
248256
force_verify_user(user)
249257
return redirect("admin:users_customuser_change", object_id)
250258

259+
def get_users_activity(self, _) -> HttpResponse:
260+
activity_prepare = UserActivityDataPreparer()
261+
xlsx_file_writer = XlsxFileToExport("активность_пользователей.xlsx")
262+
xlsx_file_writer.write_data_to_xlsx(activity_prepare.get_users_prepared_data())
263+
binary_data_to_export: bytes = xlsx_file_writer.get_binary_data_from_self_file()
264+
xlsx_file_writer.delete_self_xlsx_file_from_local_machine()
265+
266+
encoded_file_name: str = urllib.parse.quote("активность_пользователей.xlsx")
267+
response = HttpResponse(
268+
binary_data_to_export,
269+
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
270+
)
271+
response["Content-Disposition"] = f'attachment; filename*=UTF-8\'\'{encoded_file_name}'
272+
273+
return response
274+
251275
def get_export_users_emails(self, users):
252276
response_data = tablib.Dataset(
253277
headers=[

users/models.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from django.contrib.auth.models import AbstractUser
21
from django.db import models
32
from django.db.models import QuerySet
3+
from django.utils import timezone
44
from django.core.exceptions import ValidationError
5-
from django_stubs_ext.db.models import TypedModelMeta
5+
from django.contrib.auth.models import AbstractUser
66
from django.contrib.contenttypes.fields import GenericRelation
77

8-
from users import constants
8+
from django_stubs_ext.db.models import TypedModelMeta
99

10+
from users import constants
1011
from users.managers import (
1112
CustomUserManager,
1213
UserAchievementManager,
@@ -186,6 +187,13 @@ def get_project_chats(self) -> QuerySet:
186187
def get_full_name(self) -> str:
187188
return f"{self.first_name} {self.last_name}"
188189

190+
def get_user_age(self) -> int:
191+
if self.birthday is None:
192+
return None
193+
today = timezone.now()
194+
birthday = self.birthday
195+
return today.year - birthday.year - ((today.month, today.day) < (birthday.month, birthday.day))
196+
189197
def __str__(self) -> str:
190198
return f"User<{self.id}> - {self.first_name} {self.last_name}"
191199

0 commit comments

Comments
 (0)