de-view code-diff">

+ 29 - 0
commu/interfaces/post_repository.py

@@ -0,0 +1,29 @@
1
+from abc import ABC, abstractmethod
2
+
3
+class PostRepository(ABC):
4
+    
5
+    @abstractmethod
6
+    def list(self, page=1, per_page=10, order_by="-created"): pass
7
+    
8
+    @abstractmethod
9
+    def add(self, post): pass
10
+
11
+    @abstractmethod
12
+    def get(self, post_id): pass
13
+
14
+    @abstractmethod
15
+    def update(self, post):
16
+        """Update an existing post."""
17
+        pass
18
+
19
+    @abstractmethod
20
+    def delete(self, post_id):
21
+        """Delete a post by its ID."""
22
+        pass
23
+
24
+
25
+    @abstractmethod
26
+    def search(self, query: str): pass
27
+
28
+    @abstractmethod
29
+    def rate_post(self, post_id, user_id, rating: int): pass

+ 11 - 0
commu/interfaces/user_repository.py

@@ -0,0 +1,11 @@
1
+from abc import ABC, abstractmethod
2
+
3
+class UserRepository(ABC):
4
+    @abstractmethod
5
+    def add(self, user): pass
6
+
7
+    @abstractmethod
8
+    def get(self, user_id): pass
9
+
10
+    @abstractmethod
11
+    def find_by_username(self, username): pass

+ 0 - 0
commu/tests/__init__.py


BIN
commu/tests/__pycache__/__init__.cpython-310.pyc


BIN
commu/tests/__pycache__/__init__.cpython-311.pyc


BIN
commu/tests/__pycache__/test_post_repository.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_post_repository.cpython-311.pyc


BIN
commu/tests/__pycache__/test_post_service.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_post_service.cpython-311.pyc


BIN
commu/tests/__pycache__/test_user_service.cpython-310-pytest-8.4.1.pyc


BIN
commu/tests/__pycache__/test_user_service.cpython-311.pyc


+ 27 - 0
commu/tests/test_post_repository.py

@@ -0,0 +1,27 @@
1
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
2
+from commu.entities.post import Post
3
+
4
+def test_add_and_get_post():
5
+    repo = InMemoryPostRepository()
6
+    post = Post("user1", "content", images=["img.png"])
7
+    repo.add(post)
8
+    assert repo.get(post.id) == post
9
+
10
+def test_search():
11
+    repo = InMemoryPostRepository()
12
+    p1 = Post("u", "hello apple")
13
+    p2 = Post("u", "banana orange")
14
+    repo.add(p1)
15
+    repo.add(p2)
16
+    results = repo.search("apple")
17
+    assert len(results) == 1 and results[0].id == p1.id
18
+
19
+def test_rate_post():
20
+    repo = InMemoryPostRepository()
21
+    p = Post("u", "rate me")
22
+    repo.add(p)
23
+    repo.rate_post(p.id, "userA", 3)
24
+    repo.rate_post(p.id, "userB", 5)
25
+    # userA updates rating
26
+    repo.rate_post(p.id, "userA", 4)
27
+    assert p.ratings == [("userB", 5), ("userA", 4)] or p.ratings == [("userA", 4), ("userB", 5)]

+ 57 - 0
commu/tests/test_post_service.py

@@ -0,0 +1,57 @@
1
+import pytest
2
+from commu.usecases.post_service import PostService
3
+from commu.usecases.user_service import UserService
4
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
5
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
6
+from commu.entities.user import UserRole
7
+
8
+@pytest.fixture
9
+def setup_services():
10
+    user_repo = InMemoryUserRepository()
11
+    post_repo = InMemoryPostRepository()
12
+    user_svc = UserService(user_repo)
13
+    post_svc = PostService(post_repo, user_repo)
14
+    admin = user_svc.register("admin", UserRole.ADMIN)
15
+    user = user_svc.register("user", UserRole.USER)
16
+    guest = user_svc.register("guest", UserRole.GUEST)
17
+    return user_svc, post_svc, admin, user, guest
18
+
19
+def test_create_post(setup_services):
20
+    user_svc, post_svc, admin, user, guest = setup_services
21
+    post = post_svc.create_post(user.id, "My post", images=["a.jpg"])
22
+    assert post.text == "My post"
23
+    assert post.images == ["a.jpg"]
24
+
25
+def test_guest_cannot_create_post(setup_services):
26
+    user_svc, post_svc, admin, user, guest = setup_services
27
+    with pytest.raises(PermissionError):
28
+        post_svc.create_post(guest.id, "forbidden")
29
+
30
+def test_search_posts(setup_services):
31
+    user_svc, post_svc, admin, user, guest = setup_services
32
+    post1 = post_svc.create_post(user.id, "Hello world")
33
+    post2 = post_svc.create_post(user.id, "Another post")
34
+    results = post_svc.search_posts("hello")
35
+    assert len(results) == 1
36
+    assert results[0].text == "Hello world"
37
+
38
+def test_rate_post(setup_services):
39
+    user_svc, post_svc, admin, user, guest = setup_services
40
+    post = post_svc.create_post(user.id, "My post")
41
+    post_svc.rate_post(post.id, admin.id, 5)
42
+    post_svc.rate_post(post.id, user.id, 4)
43
+    assert abs(post.average_rating() - 4.5) < 1e-6
44
+
45
+def test_guest_cannot_rate(setup_services):
46
+    user_svc, post_svc, admin, user, guest = setup_services
47
+    post = post_svc.create_post(user.id, "Guest can't rate")
48
+    with pytest.raises(PermissionError):
49
+        post_svc.rate_post(post.id, guest.id, 5)
50
+
51
+def test_rating_bounds(setup_services):
52
+    user_svc, post_svc, admin, user, guest = setup_services
53
+    post = post_svc.create_post(user.id, "Test bounds")
54
+    with pytest.raises(ValueError):
55
+        post_svc.rate_post(post.id, admin.id, 0)
56
+    with pytest.raises(ValueError):
57
+        post_svc.rate_post(post.id, admin.id, 6)

+ 25 - 0
commu/tests/test_user_service.py

@@ -0,0 +1,25 @@
1
+import pytest
2
+from commu.usecases.user_service import UserService
3
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
4
+from commu.entities.user import UserRole
5
+
6
+def test_register_user():
7
+    repo = InMemoryUserRepository()
8
+    svc = UserService(repo)
9
+    user = svc.register("testuser")
10
+    assert user.username == "testuser"
11
+    assert user.role == UserRole.USER
12
+    assert repo.get(user.id) == user
13
+
14
+def test_register_admin():
15
+    repo = InMemoryUserRepository()
16
+    svc = UserService(repo)
17
+    user = svc.register("adminuser", UserRole.ADMIN)
18
+    assert user.role == UserRole.ADMIN
19
+
20
+def test_get_user():
21
+    repo = InMemoryUserRepository()
22
+    svc = UserService(repo)
23
+    user = svc.register("abc")
24
+    found = svc.get_user(user.id)
25
+    assert found.username == "abc"

+ 0 - 0
commu/usecases/__init__.py


BIN
commu/usecases/__pycache__/__init__.cpython-310.pyc


BIN
commu/usecases/__pycache__/__init__.cpython-311.pyc


BIN
commu/usecases/__pycache__/post_service.cpython-310.pyc


BIN
commu/usecases/__pycache__/post_service.cpython-311.pyc


BIN
commu/usecases/__pycache__/user_service.cpython-310.pyc


BIN
commu/usecases/__pycache__/user_service.cpython-311.pyc


+ 28 - 0
commu/usecases/post_service.py

@@ -0,0 +1,28 @@
1
+from commu.entities.post import Post
2
+from commu.entities.user import UserRole
3
+
4
+class PostService:
5
+    def __init__(self, post_repo, user_repo):
6
+        self.post_repo = post_repo
7
+        self.user_repo = user_repo
8
+
9
+    def create_post(self, author_id, text, images=None, videos=None):
10
+        user = self.user_repo.get(author_id)
11
+        print(f"user = {user}")
12
+        print(f"role = {user.role}")
13
+        if not user or user.role == UserRole.GUEST:
14
+            raise PermissionError("User does not have permission to create posts")
15
+        post = Post(author_id, text, images, videos)
16
+        self.post_repo.add(post)
17
+        return post
18
+
19
+    def search_posts(self, query):
20
+        return self.post_repo.search(query)
21
+
22
+    def rate_post(self, post_id, user_id, rating):
23
+        user = self.user_repo.get(user_id)
24
+        if not user or user.role == UserRole.GUEST:
25
+            raise PermissionError("Guests cannot rate posts")
26
+        if not (1 <= rating <= 5):
27
+            raise ValueError("Rating must be between 1 and 5")
28
+        self.post_repo.rate_post(post_id, user_id, rating)

+ 13 - 0
commu/usecases/user_service.py

@@ -0,0 +1,13 @@
1
+from commu.entities.user import User, UserRole
2
+
3
+class UserService:
4
+    def __init__(self, user_repo):
5
+        self.user_repo = user_repo
6
+
7
+    def register(self, username, role=UserRole.USER):
8
+        user = User(username, role)
9
+        self.user_repo.add(user)
10
+        return user
11
+
12
+    def get_user(self, user_id):
13
+        return self.user_repo.get(user_id)

+ 0 - 0
core/__init__.py


BIN
core/__pycache__/__init__.cpython-311.pyc


BIN
core/__pycache__/admin.cpython-311.pyc


BIN
core/__pycache__/apps.cpython-311.pyc


BIN
core/__pycache__/models.cpython-311.pyc


BIN
core/__pycache__/tests.cpython-311.pyc


BIN
core/__pycache__/urls.cpython-311.pyc


BIN
core/__pycache__/views.cpython-311.pyc


+ 3 - 0
core/admin.py

@@ -0,0 +1,3 @@
1
+from django.contrib import admin
2
+
3
+# Register your models here.

+ 6 - 0
core/apps.py

@@ -0,0 +1,6 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class CoreConfig(AppConfig):
5
+    default_auto_field = 'django.db.models.BigAutoField'
6
+    name = 'core'

+ 38 - 0
core/migrations/0001_initial.py

@@ -0,0 +1,38 @@
1
+# Generated by Django 5.2.4 on 2025-08-03 16:36
2
+
3
+import django.db.models.deletion
4
+from django.conf import settings
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='Post',
19
+            fields=[
20
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('text', models.TextField()),
22
+                ('images', models.JSONField(blank=True, default=list)),
23
+                ('videos', models.JSONField(blank=True, default=list)),
24
+                ('created', models.DateTimeField(auto_now_add=True)),
25
+                ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
26
+            ],
27
+        ),
28
+        migrations.CreateModel(
29
+            name='PostRating',
30
+            fields=[
31
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+                ('rating', models.IntegerField()),
33
+                ('created', models.DateTimeField(auto_now_add=True)),
34
+                ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='core.post')),
35
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
36
+            ],
37
+        ),
38
+    ]

+ 0 - 0
core/migrations/__init__.py


BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc


BIN
core/migrations/__pycache__/__init__.cpython-311.pyc


+ 18 - 0
core/models.py

@@ -0,0 +1,18 @@
1
+from django.db import models
2
+from django.contrib.auth import get_user_model
3
+
4
+User = get_user_model()
5
+
6
+class Post(models.Model):
7
+    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
8
+    text = models.TextField()
9
+    images = models.JSONField(default=list, blank=True)
10
+    videos = models.JSONField(default=list, blank=True)
11
+    created = models.DateTimeField(auto_now_add=True)
12
+
13
+class PostRating(models.Model):
14
+    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="ratings")
15
+    user = models.ForeignKey(User, on_delete=models.CASCADE)
16
+    rating = models.IntegerField()
17
+    created = models.DateTimeField(auto_now_add=True)
18
+

BIN
core/repositories/__pycache__/django_post_repository.cpython-311.pyc


BIN
core/repositories/__pycache__/django_user_repository.cpython-311.pyc


+ 112 - 0
core/repositories/django_post_repository.py

@@ -0,0 +1,112 @@
1
+from commu.interfaces.post_repository import PostRepository
2
+from core.models import Post, PostRating
3
+from django.contrib.auth import get_user_model
4
+
5
+User = get_user_model()
6
+
7
+class DjangoPostRepository(PostRepository):
8
+    def add(self, post):
9
+        # post: commu.entities.post.Post
10
+        try:
11
+            author = User.objects.get(id=post.author_id)
12
+        except User.DoesNotExist:
13
+            raise ValueError("Author not found")
14
+        obj = Post.objects.create(
15
+            author=author,
16
+            text=post.text,
17
+            images=post.images,
18
+            videos=post.videos,
19
+        )
20
+        post.id = str(obj.id)
21
+        return post
22
+
23
+    def get(self, post_id):
24
+        try:
25
+            obj = Post.objects.get(id=post_id)
26
+            from commu.entities.post import Post as DomainPost
27
+            domain_post = DomainPost(
28
+                author_id=str(obj.author.id),
29
+                text=obj.text,
30
+                images=obj.images,
31
+                videos=obj.videos,
32
+            )
33
+            domain_post.id = str(obj.id)
34
+            # Load ratings
35
+            domain_post.ratings = [
36
+                (str(r.user.id), r.rating)
37
+                for r in obj.ratings.all()
38
+            ]
39
+            return domain_post
40
+        except Post.DoesNotExist:
41
+            return None
42
+
43
+    def search(self, query: str):
44
+        qs = Post.objects.filter(text__icontains=query)
45
+        from commu.entities.post import Post as DomainPost
46
+        result = []
47
+        for obj in qs:
48
+            domain_post = DomainPost(
49
+                author_id=str(obj.author.id),
50
+                text=obj.text,
51
+                images=obj.images,
52
+                videos=obj.videos,
53
+            )
54
+            domain_post.id = str(obj.id)
55
+            # Load ratings
56
+            domain_post.ratings = [
57
+                (str(r.user.id), r.rating)
58
+                for r in obj.ratings.all()
59
+            ]
60
+            result.append(domain_post)
61
+        return result
62
+
63
+    def rate_post(self, post_id, user_id, rating: int):
64
+        try:
65
+            post = Post.objects.get(id=post_id)
66
+            user = User.objects.get(id=user_id)
67
+        except (Post.DoesNotExist, User.DoesNotExist):
68
+            raise ValueError("Post or user not found")
69
+        # Remove old rating from user, if any
70
+        PostRating.objects.filter(post=post, user=user).delete()
71
+        # Create new rating
72
+        PostRating.objects.create(post=post, user=user, rating=rating)
73
+
74
+    def delete(self, post_id):
75
+        try:
76
+            obj = Post.objects.get(id=post_id)
77
+            obj.delete()
78
+        except Post.DoesNotExist:
79
+            raise ValueError("Post not found")
80
+
81
+    def list(self, page=1, per_page=10, order_by="-created"):
82
+        qs = Post.objects.all().order_by(order_by)
83
+        total = qs.count()
84
+        start = (page - 1) * per_page
85
+        end = start + per_page
86
+        from commu.entities.post import Post as DomainPost
87
+        posts = []
88
+        for obj in qs[start:end]:
89
+            domain_post = DomainPost(
90
+                author_id=str(obj.author.id),
91
+                text=obj.text,
92
+                images=obj.images,
93
+                videos=obj.videos,
94
+            )
95
+            domain_post.id = str(obj.id)
96
+            domain_post.ratings = [
97
+                (str(r.user.id), r.rating)
98
+                for r in obj.ratings.all()
99
+            ]
100
+            posts.append(domain_post)
101
+        return posts, total
102
+
103
+    def update(self, post):
104
+        try:
105
+            obj = Post.objects.get(id=post.id)
106
+            obj.text = post.text
107
+            obj.images = post.images
108
+            obj.videos = post.videos
109
+            obj.save()
110
+            # Optionally update ratings (if part of update)
111
+        except Post.DoesNotExist:
112
+            raise ValueError("Post not found")

+ 55 - 0
core/repositories/django_user_repository.py

@@ -0,0 +1,55 @@
1
+from commu.interfaces.user_repository import UserRepository
2
+from django.contrib.auth.models import User as DjangoUser
3
+
4
+class DjangoUserRepository(UserRepository):
5
+    def add(self, user):
6
+        # user: commu.entities.user.User
7
+        # Map role to Django flags
8
+        is_staff = user.role.value in ("admin", "user") if hasattr(user.role, "value") else user.role in ("admin", "user")
9
+        is_superuser = user.role.value == "admin" if hasattr(user.role, "value") else user.role == "admin"
10
+        obj = DjangoUser.objects.create_user(
11
+            username=user.username,
12
+            password=None,  # You may set/generate a password as needed!
13
+            is_staff=is_staff,
14
+            is_superuser=is_superuser,
15
+        )
16
+        user.id = str(obj.id)
17
+        return user
18
+
19
+    def get(self, user_id):
20
+        try:
21
+            obj = DjangoUser.objects.get(id=user_id)
22
+            from commu.entities.user import User, UserRole
23
+            role = self._role_from_django(obj)
24
+            u = User(
25
+                username=obj.username,
26
+                role=role,
27
+            )
28
+            u.id = str(obj.id)
29
+            return u
30
+        except DjangoUser.DoesNotExist:
31
+            return None
32
+
33
+    def find_by_username(self, username):
34
+        try:
35
+            obj = DjangoUser.objects.get(username=username)
36
+            from commu.entities.user import User, UserRole
37
+            role = self._role_from_django(obj)
38
+            u = User(
39
+                username=obj.username,
40
+                role=role,
41
+            )
42
+            u.id = str(obj.id)
43
+            return u
44
+        except DjangoUser.DoesNotExist:
45
+            return None
46
+
47
+    def _role_from_django(self, user_obj):
48
+        from commu.entities.user import UserRole
49
+        if user_obj.is_superuser:
50
+            return UserRole.ADMIN
51
+        elif user_obj.is_staff:
52
+            return UserRole.USER
53
+        else:
54
+            return UserRole.GUEST
55
+

+ 3 - 0
core/tests.py

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 0 - 0
core/tests/__init__.py


BIN
core/tests/__pycache__/__init__.cpython-311.pyc


BIN
core/tests/__pycache__/test_post_service_django.cpython-311.pyc


+ 60 - 0
core/tests/test_post_service_django.py

@@ -0,0 +1,60 @@
1
+import pytest
2
+from django.test import TestCase
3
+from django.contrib.auth import get_user_model
4
+
5
+from commu.usecases.post_service import PostService
6
+from commu.entities.user import UserRole
7
+from core.repositories.django_post_repository import DjangoPostRepository
8
+from core.repositories.django_user_repository import DjangoUserRepository
9
+
10
+User = get_user_model()
11
+
12
+class PostServiceIntegrationTest(TestCase):
13
+
14
+    def setUp(self):
15
+        # Use Django's repositories
16
+        self.user_repo = DjangoUserRepository()
17
+        self.post_repo = DjangoPostRepository()
18
+        self.service = PostService(self.post_repo, self.user_repo)
19
+
20
+        # Create some users using Django ORM
21
+        self.admin = User.objects.create_user(username="admin", is_superuser=True, is_staff=True)
22
+        self.user = User.objects.create_user(username="user", is_staff=True)
23
+        self.guest = User.objects.create_user(username="guest")  # Not staff or superuser
24
+
25
+    def test_create_post_by_user(self):
26
+        post = self.service.create_post(str(self.user.id), "Hello Django!", ["img1.png"], ["vid1.mp4"])
27
+        assert post.text == "Hello Django!"
28
+        assert post.images == ["img1.png"]
29
+        assert post.videos == ["vid1.mp4"]
30
+
31
+    def test_guest_cannot_create_post(self):
32
+        with self.assertRaises(PermissionError):
33
+            self.service.create_post(str(self.guest.id), "Forbidden")
34
+
35
+    def test_search_posts(self):
36
+        p1 = self.service.create_post(str(self.user.id), "First Django post")
37
+        p2 = self.service.create_post(str(self.user.id), "Second hello world")
38
+        results = self.service.search_posts("hello")
39
+        assert len(results) == 1
40
+        assert results[0].text == "Second hello world"
41
+
42
+    def test_rate_post(self):
43
+        post = self.service.create_post(str(self.user.id), "Rate me Django!")
44
+        self.service.rate_post(post.id, str(self.admin.id), 5)
45
+        self.service.rate_post(post.id, str(self.user.id), 4)
46
+        updated_post = self.post_repo.get(post.id)
47
+        assert abs(updated_post.average_rating() - 4.5) < 1e-6
48
+
49
+    def test_guest_cannot_rate_post(self):
50
+        post = self.service.create_post(str(self.user.id), "No guest rate")
51
+        with self.assertRaises(PermissionError):
52
+            self.service.rate_post(post.id, str(self.guest.id), 2)
53
+
54
+    def test_rating_bounds(self):
55
+        post = self.service.create_post(str(self.user.id), "Bounds Django")
56
+        with self.assertRaises(ValueError):
57
+            self.service.rate_post(post.id, str(self.admin.id), 0)
58
+        with self.assertRaises(ValueError):
59
+            self.service.rate_post(post.id, str(self.admin.id), 6)
60
+

+ 8 - 0
core/urls.py

@@ -0,0 +1,8 @@
1
+from django.urls import path
2
+from . import views
3
+
4
+urlpatterns = [
5
+    path('posts/create/', views.create_post, name='create_post'),
6
+    path('posts/search/', views.search_posts, name='search_posts'),
7
+    path('posts/rate/', views.rate_post, name='rate_post'),
8
+]

+ 69 - 0
core/views.py

@@ -0,0 +1,69 @@
1
+import json
2
+from django.http import JsonResponse, HttpResponseBadRequest
3
+from django.views.decorators.csrf import csrf_exempt
4
+from django.contrib.auth.decorators import login_required
5
+from commu.usecases.post_service import PostService
6
+from core.repositories.django_post_repository import DjangoPostRepository
7
+from core.repositories.django_user_repository import DjangoUserRepository
8
+
9
+# Service setup (could be moved to app config for larger apps)
10
+user_repo = DjangoUserRepository()
11
+post_repo = DjangoPostRepository()
12
+post_service = PostService(post_repo, user_repo)
13
+
14
+@csrf_exempt
15
+@login_required
16
+def create_post(request):
17
+    if request.method != 'POST':
18
+        return HttpResponseBadRequest("POST only")
19
+    try:
20
+        data = json.loads(request.body.decode())
21
+        text = data.get("text")
22
+        images = data.get("images", [])
23
+        videos = data.get("videos", [])
24
+        if not text:
25
+            return JsonResponse({"error": "Missing post text"}, status=400)
26
+        # Use logged-in Django user as author
27
+        user_id = str(request.user.id)
28
+        post = post_service.create_post(user_id, text, images, videos)
29
+        return JsonResponse({
30
+            "id": post.id,
31
+            "text": post.text,
32
+            "author_id": post.author_id,
33
+            "images": post.images,
34
+            "videos": post.videos
35
+        })
36
+    except Exception as e:
37
+        return JsonResponse({"error": str(e)}, status=400)
38
+
39
+def search_posts(request):
40
+    q = request.GET.get("q", "")
41
+    results = post_service.search_posts(q)
42
+    posts = [{
43
+        "id": p.id,
44
+        "text": p.text,
45
+        "author_id": p.author_id,
46
+        "images": p.images,
47
+        "videos": p.videos,
48
+        "avg_rating": p.average_rating()
49
+    } for p in results]
50
+    return JsonResponse({"results": posts})
51
+
52
+@csrf_exempt
53
+@login_required
54
+def rate_post(request):
55
+    if request.method != 'POST':
56
+        return HttpResponseBadRequest("POST only")
57
+    try:
58
+        data = json.loads(request.body.decode())
59
+        post_id = data.get("post_id")
60
+        rating = data.get("rating")
61
+        if not post_id or rating is None:
62
+            return JsonResponse({"error": "Missing post_id or rating"}, status=400)
63
+        user_id = str(request.user.id)
64
+        post_service.rate_post(post_id, user_id, int(rating))
65
+        return JsonResponse({"ok": True})
66
+    except Exception as e:
67
+        return JsonResponse({"error": str(e)}, status=400)
68
+
69
+

+ 0 - 0
db.sqlite3


+ 0 - 0
django_commu/__init__.py


BIN
django_commu/__pycache__/__init__.cpython-311.pyc


BIN
django_commu/__pycache__/settings.cpython-311.pyc


BIN
django_commu/__pycache__/urls.cpython-311.pyc


BIN
django_commu/__pycache__/wsgi.cpython-311.pyc


+ 16 - 0
django_commu/asgi.py

@@ -0,0 +1,16 @@
1
+"""
2
+ASGI config for django_commu project.
3
+
4
+It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.asgi import get_asgi_application
13
+
14
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
15
+
16
+application = get_asgi_application()

+ 128 - 0
django_commu/settings.py

@@ -0,0 +1,128 @@
1
+"""
2
+Django settings for django_commu project.
3
+
4
+Generated by 'django-admin startproject' using Django 5.2.4.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/topics/settings/
8
+
9
+For the full list of settings and their values, see
10
+https://docs.djangoproject.com/en/5.2/ref/settings/
11
+"""
12
+
13
+from pathlib import Path
14
+import os
15
+
16
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
17
+BASE_DIR = Path(__file__).resolve().parent.parent
18
+
19
+
20
+# Quick-start development settings - unsuitable for production
21
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
22
+
23
+# SECURITY WARNING: keep the secret key used in production secret!
24
+SECRET_KEY = 'django-insecure-c$ieo22nwe^v#*e$xlh@fath9*t+f8wtqrt9e!op9-ittc=b_x'
25
+
26
+# SECURITY WARNING: don't run with debug turned on in production!
27
+DEBUG = True
28
+
29
+ALLOWED_HOSTS = []
30
+
31
+
32
+# Application definition
33
+
34
+INSTALLED_APPS = [
35
+    'django.contrib.admin',
36
+    'django.contrib.auth',
37
+    'django.contrib.contenttypes',
38
+    'django.contrib.sessions',
39
+    'django.contrib.messages',
40
+    'django.contrib.staticfiles',
41
+    'core',
42
+]
43
+
44
+MIDDLEWARE = [
45
+    'django.middleware.security.SecurityMiddleware',
46
+    'django.contrib.sessions.middleware.SessionMiddleware',
47
+    'django.middleware.common.CommonMiddleware',
48
+    'django.middleware.csrf.CsrfViewMiddleware',
49
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
50
+    'django.contrib.messages.middleware.MessageMiddleware',
51
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
52
+]
53
+
54
+ROOT_URLCONF = 'django_commu.urls'
55
+
56
+TEMPLATES = [
57
+    {
58
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
59
+        'DIRS': [],
60
+        'APP_DIRS': True,
61
+        'OPTIONS': {
62
+            'context_processors': [
63
+                'django.template.context_processors.request',
64
+                'django.contrib.auth.context_processors.auth',
65
+                'django.contrib.messages.context_processors.messages',
66
+            ],
67
+        },
68
+    },
69
+]
70
+
71
+WSGI_APPLICATION = 'django_commu.wsgi.application'
72
+
73
+
74
+# Database
75
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
76
+
77
+DATABASES = {
78
+    'default': {
79
+        'ENGINE': 'django.db.backends.postgresql',
80
+        'NAME': os.getenv('DB_NAME', 'commudb'),
81
+        'USER': os.getenv('DB_USER', 'commuuser'),
82
+        'PASSWORD': os.getenv('DB_PASSWORD', 'commupass'),
83
+        'HOST': os.getenv('DB_HOST', 'db'),
84
+        'PORT': os.getenv('DB_PORT', '5432'),
85
+    }
86
+}
87
+
88
+
89
+# Password validation
90
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
91
+
92
+AUTH_PASSWORD_VALIDATORS = [
93
+    {
94
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
95
+    },
96
+    {
97
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
98
+    },
99
+    {
100
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
101
+    },
102
+    {
103
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
104
+    },
105
+]
106
+
107
+
108
+# Internationalization
109
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
110
+
111
+LANGUAGE_CODE = 'en-us'
112
+
113
+TIME_ZONE = 'UTC'
114
+
115
+USE_I18N = True
116
+
117
+USE_TZ = True
118
+
119
+
120
+# Static files (CSS, JavaScript, Images)
121
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
122
+
123
+STATIC_URL = 'static/'
124
+
125
+# Default primary key field type
126
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
127
+
128
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

+ 23 - 0
django_commu/urls.py

@@ -0,0 +1,23 @@
1
+"""
2
+URL configuration for django_commu project.
3
+
4
+The `urlpatterns` list routes URLs to views. For more information please see:
5
+    https://docs.djangoproject.com/en/5.2/topics/http/urls/
6
+Examples:
7
+Function views
8
+    1. Add an import:  from my_app import views
9
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
10
+Class-based views
11
+    1. Add an import:  from other_app.views import Home
12
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
13
+Including another URLconf
14
+    1. Import the include() function: from django.urls import include, path
15
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
16
+"""
17
+from django.contrib import admin
18
+from django.urls import path, include
19
+
20
+urlpatterns = [
21
+    path('admin/', admin.site.urls),
22
+    path('core/', include('core.urls')),
23
+]

+ 16 - 0
django_commu/wsgi.py

@@ -0,0 +1,16 @@
1
+"""
2
+WSGI config for django_commu project.
3
+
4
+It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.wsgi import get_wsgi_application
13
+
14
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
15
+
16
+application = get_wsgi_application()

+ 40 - 0
docker-compose.yml

@@ -0,0 +1,40 @@
1
+version: '3.9'
2
+
3
+services:
4
+  db:
5
+    image: postgres:16
6
+    environment:
7
+      POSTGRES_DB: commudb
8
+      POSTGRES_USER: commuuser
9
+      POSTGRES_PASSWORD: commupass
10
+    ports:
11
+      - "5432:5432"
12
+    volumes:
13
+      - postgres_data:/var/lib/postgresql/data
14
+
15
+  web:
16
+    build:
17
+      context: .
18
+      args:
19
+        UID: ${UID}
20
+        GID: ${GID}
21
+    user: "${UID}:${GID}"
22
+    command: >
23
+      sh -c "python manage.py runserver 0.0.0.0:8000"
24
+    volumes:
25
+      - .:/app
26
+    working_dir: /app
27
+    ports:
28
+      - "8000:8000"
29
+    depends_on:
30
+      - db
31
+    environment:
32
+      - DEBUG=1
33
+      - DB_NAME=commudb
34
+      - DB_USER=commuuser
35
+      - DB_PASSWORD=commupass
36
+      - DB_HOST=db
37
+      - DB_PORT=5432
38
+
39
+volumes:
40
+  postgres_data:

+ 38 - 0
main.py

@@ -0,0 +1,38 @@
1
+from commu.infrastructure.memory_user_repository import InMemoryUserRepository
2
+from commu.infrastructure.memory_post_repository import InMemoryPostRepository
3
+from commu.usecases.user_service import UserService
4
+from commu.usecases.post_service import PostService
5
+from commu.entities.user import UserRole
6
+
7
+def main():
8
+    user_repo = InMemoryUserRepository()
9
+    post_repo = InMemoryPostRepository()
10
+    user_service = UserService(user_repo)
11
+    post_service = PostService(post_repo, user_repo)
12
+
13
+    # Create users
14
+    admin = user_service.register("alice", UserRole.ADMIN)
15
+    user = user_service.register("bob", UserRole.USER)
16
+    guest = user_service.register("guest", UserRole.GUEST)
17
+
18
+    # User creates post
19
+    post = post_service.create_post(user.id, "Hello world!", images=["img1.png"], videos=["vid1.mp4"])
20
+    print(f"Created Post: {post.text}, by {user.username}")
21
+
22
+    # Post search
23
+    found = post_service.search_posts("hello")
24
+    print(f"Found {len(found)} posts containing 'hello'.")
25
+
26
+    # Post rating
27
+    post_service.rate_post(post.id, admin.id, 5)
28
+    post_service.rate_post(post.id, user.id, 4)
29
+    print(f"Average Rating: {post.average_rating()}")
30
+
31
+    # Guest tries to create post (should fail)
32
+    try:
33
+        post_service.create_post(guest.id, "I am a guest")
34
+    except PermissionError as e:
35
+        print("Guest cannot create post:", e)
36
+
37
+if __name__ == "__main__":
38
+    main()

+ 22 - 0
manage.py

@@ -0,0 +1,22 @@
1
+#!/usr/bin/env python
2
+"""Django's command-line utility for administrative tasks."""
3
+import os
4
+import sys
5
+
6
+
7
+def main():
8
+    """Run administrative tasks."""
9
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_commu.settings')
10
+    try:
11
+        from django.core.management import execute_from_command_line
12
+    except ImportError as exc:
13
+        raise ImportError(
14
+            "Couldn't import Django. Are you sure it's installed and "
15
+            "available on your PYTHONPATH environment variable? Did you "
16
+            "forget to activate a virtual environment?"
17
+        ) from exc
18
+    execute_from_command_line(sys.argv)
19
+
20
+
21
+if __name__ == '__main__':
22
+    main()

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
1
+pytest
2
+Django>=4.2
3
+psycopg2-binary

tum/tmt_learning - Gogs: Simplico Git Service

Нет описания

readme.markdown 11KB

resolve Version Badge

implements the node require.resolve() algorithm such that you can require.resolve() on behalf of a file asynchronously and synchronously

github actions coverage dependency status dev dependency status License Downloads

npm badge

example

asynchronously resolve:

var resolve = require('resolve/async'); // or, require('resolve')
resolve('tap', { basedir: __dirname }, function (err, res) {
    if (err) console.error(err);
    else console.log(res);
});
$ node example/async.js
/home/substack/projects/node-resolve/node_modules/tap/lib/main.js

synchronously resolve:

var resolve = require('resolve/sync'); // or, `require('resolve').sync
var res = resolve('tap', { basedir: __dirname });
console.log(res);
$ node example/sync.js
/home/substack/projects/node-resolve/node_modules/tap/lib/main.js

methods

var resolve = require('resolve');
var async = require('resolve/async');
var sync = require('resolve/sync');

For both the synchronous and asynchronous methods, errors may have any of the following err.code values:

  • MODULE_NOT_FOUND: the given path string (id) could not be resolved to a module
  • INVALID_BASEDIR: the specified opts.basedir doesn't exist, or is not a directory
  • INVALID_PACKAGE_MAIN: a package.json was encountered with an invalid main property (eg. not a string)

resolve(id, opts={}, cb)

Asynchronously resolve the module path string id into cb(err, res [, pkg]), where pkg (if defined) is the data from package.json.

options are:

  • opts.basedir - directory to begin resolving from

  • opts.package - package.json data applicable to the module being loaded

  • opts.extensions - array of file extensions to search in order

  • opts.includeCoreModules - set to false to exclude node core modules (e.g. fs) from the search

  • opts.readFile - how to read files asynchronously

  • opts.isFile - function to asynchronously test whether a file exists

  • opts.isDirectory - function to asynchronously test whether a file exists and is a directory

  • opts.realpath - function to asynchronously resolve a potential symlink to its real path

  • opts.readPackage(readFile, pkgfile, cb) - function to asynchronously read and parse a package.json file

    • readFile - the passed opts.readFile or fs.readFile if not specified
    • pkgfile - path to package.json
    • cb - callback
  • opts.packageFilter(pkg, pkgfile, dir) - transform the parsed package.json contents before looking at the "main" field

    • pkg - package data
    • pkgfile - path to package.json
    • dir - directory that contains package.json
  • opts.pathFilter(pkg, path, relativePath) - transform a path within a package

    • pkg - package data
    • path - the path being resolved
    • relativePath - the path relative from the package.json location
    • returns - a relative path that will be joined from the package.json location
  • opts.paths - require.paths array to use if nothing is found on the normal node_modules recursive walk (probably don't use this)

For advanced users, paths can also be a opts.paths(request, start, opts) function

* request - the import specifier being resolved
* start - lookup path
* getNodeModulesDirs - a thunk (no-argument function) that returns the paths using standard `node_modules` resolution
* opts - the resolution options
  • opts.packageIterator(request, start, opts) - return the list of candidate paths where the packages sources may be found (probably don't use this)

    • request - the import specifier being resolved
    • start - lookup path
    • getPackageCandidates - a thunk (no-argument function) that returns the paths using standard node_modules resolution
    • opts - the resolution options
  • opts.moduleDirectory - directory (or directories) in which to recursively look for modules. default: "node_modules"

  • opts.preserveSymlinks - if true, doesn't resolve basedir to real path before resolving. This is the way Node resolves dependencies when executed with the --preserve-symlinks flag. Note: this property is currently true by default but it will be changed to false in the next major version because Node's resolution algorithm does not preserve symlinks by default.

default opts values:

{
    paths: [],
    basedir: __dirname,
    extensions: ['.js'],
    includeCoreModules: true,
    readFile: fs.readFile,
    isFile: function isFile(file, cb) {
        fs.stat(file, function (err, stat) {
            if (!err) {
                return cb(null, stat.isFile() || stat.isFIFO());
            }
            if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
            return cb(err);
        });
    },
    isDirectory: function isDirectory(dir, cb) {
        fs.stat(dir, function (err, stat) {
            if (!err) {
                return cb(null, stat.isDirectory());
            }
            if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
            return cb(err);
        });
    },
    realpath: function realpath(file, cb) {
        var realpath = typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath;
        realpath(file, function (realPathErr, realPath) {
            if (realPathErr && realPathErr.code !== 'ENOENT') cb(realPathErr);
            else cb(null, realPathErr ? file : realPath);
        });
    },
    readPackage: function defaultReadPackage(readFile, pkgfile, cb) {
        readFile(pkgfile, function (readFileErr, body) {
            if (readFileErr) cb(readFileErr);
            else {
                try {
                    var pkg = JSON.parse(body);
                    cb(null, pkg);
                } catch (jsonErr) {
                    cb(null);
                }
            }
        });
    },
    moduleDirectory: 'node_modules',
    preserveSymlinks: true
}

resolve.sync(id, opts)

Synchronously resolve the module path string id, returning the result and throwing an error when id can't be resolved.

options are:

  • opts.basedir - directory to begin resolving from

  • opts.extensions - array of file extensions to search in order

  • opts.includeCoreModules - set to false to exclude node core modules (e.g. fs) from the search

  • opts.readFileSync - how to read files synchronously

  • opts.isFile - function to synchronously test whether a file exists

  • opts.isDirectory - function to synchronously test whether a file exists and is a directory

  • opts.realpathSync - function to synchronously resolve a potential symlink to its real path

  • opts.readPackageSync(readFileSync, pkgfile) - function to synchronously read and parse a package.json file

    • readFileSync - the passed opts.readFileSync or fs.readFileSync if not specified
    • pkgfile - path to package.json
  • opts.packageFilter(pkg, dir) - transform the parsed package.json contents before looking at the "main" field

    • pkg - package data
    • dir - directory that contains package.json (Note: the second argument will change to "pkgfile" in v2)
  • opts.pathFilter(pkg, path, relativePath) - transform a path within a package

    • pkg - package data
    • path - the path being resolved
    • relativePath - the path relative from the package.json location
    • returns - a relative path that will be joined from the package.json location
  • opts.paths - require.paths array to use if nothing is found on the normal node_modules recursive walk (probably don't use this)

For advanced users, paths can also be a opts.paths(request, start, opts) function

* request - the import specifier being resolved
* start - lookup path
* getNodeModulesDirs - a thunk (no-argument function) that returns the paths using standard `node_modules` resolution
* opts - the resolution options
  • opts.packageIterator(request, start, opts) - return the list of candidate paths where the packages sources may be found (probably don't use this)

    • request - the import specifier being resolved
    • start - lookup path
    • getPackageCandidates - a thunk (no-argument function) that returns the paths using standard node_modules resolution
    • opts - the resolution options
  • opts.moduleDirectory - directory (or directories) in which to recursively look for modules. default: "node_modules"

  • opts.preserveSymlinks - if true, doesn't resolve basedir to real path before resolving. This is the way Node resolves dependencies when executed with the --preserve-symlinks flag. Note: this property is currently true by default but it will be changed to false in the next major version because Node's resolution algorithm does not preserve symlinks by default.

default opts values:

{
    paths: [],
    basedir: __dirname,
    extensions: ['.js'],
    includeCoreModules: true,
    readFileSync: fs.readFileSync,
    isFile: function isFile(file) {
        try {
            var stat = fs.statSync(file);
        } catch (e) {
            if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return false;
            throw e;
        }
        return stat.isFile() || stat.isFIFO();
    },
    isDirectory: function isDirectory(dir) {
        try {
            var stat = fs.statSync(dir);
        } catch (e) {
            if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return false;
            throw e;
        }
        return stat.isDirectory();
    },
    realpathSync: function realpathSync(file) {
        try {
            var realpath = typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync;
            return realpath(file);
        } catch (realPathErr) {
            if (realPathErr.code !== 'ENOENT') {
                throw realPathErr;
            }
        }
        return file;
    },
    readPackageSync: function defaultReadPackageSync(readFileSync, pkgfile) {
        var body = readFileSync(pkgfile);
        try {
            var pkg = JSON.parse(body);
            return pkg;
        } catch (jsonErr) {}
    },
    moduleDirectory: 'node_modules',
    preserveSymlinks: true
}

install

With npm do:

npm install resolve

license

MIT