From e093b6f6cec6d1f75d83fa5d56d3456b0fff71b0 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Wed, 3 Dec 2025 16:33:22 +0330 Subject: [PATCH] fix --- apps/api/views/documentation.py | 63 ++++++++++++++----- apps/article/serializers.py | 5 +- apps/article/views.py | 60 +++++++++++------- apps/hadis/serializers/hadis.py | 2 +- apps/library/doc.py | 47 ++++++++++++-- .../migrations/0007_auto_20251203_1529.py | 18 ++++++ .../migrations/0008_auto_20251203_1533.py | 30 +++++++++ apps/library/models.py | 10 ++- apps/library/serializers.py | 3 +- apps/library/views.py | 30 ++++++++- apps/podcast/serializers.py | 11 +++- apps/podcast/views.py | 49 ++++++++++----- 12 files changed, 262 insertions(+), 66 deletions(-) create mode 100644 apps/library/migrations/0007_auto_20251203_1529.py create mode 100644 apps/library/migrations/0008_auto_20251203_1533.py diff --git a/apps/api/views/documentation.py b/apps/api/views/documentation.py index 3c06908..3bf7280 100644 --- a/apps/api/views/documentation.py +++ b/apps/api/views/documentation.py @@ -296,10 +296,13 @@ class CustomAPIDocumentationView(View): 'name': 'Book List', 'method': 'GET', 'url': '/api/library/books/', - 'description': 'Get paginated list of books', + 'description': 'Get paginated list of books with filtering and sorting', 'parameters': [ - {'name': 'category', 'type': 'integer', 'description': 'Filter by category ID', 'required': False}, - {'name': 'search', 'type': 'string', 'description': 'Search in book titles and authors', 'required': False}, + {'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False}, + {'name': 'collection_id', 'type': 'integer', 'description': 'Filter by collection ID', 'required': False}, + {'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked books (true/false)', 'required': False}, + {'name': 'search', 'type': 'string', 'description': 'Search in book titles, summary, publisher, or ISBN', 'required': False}, + {'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, download_count, -download_count, title, -title, pin, -pin, -pin,-created_at', 'required': False}, ], 'response_examples': { 'success': json.dumps({ @@ -308,13 +311,28 @@ class CustomAPIDocumentationView(View): { "id": 1, "title": "Al-Kafi", + "slug": "al-kafi", "author": "Muhammad ibn Ya'qub al-Kulayni", - "description": "One of the most important Shia hadith collections", - "cover_image": "https://example.com/media/books/alkafi.jpg", - "file_size": "15.2 MB", - "pages": 1200, - "language": "Arabic", - "download_count": 2456 + "publisher": "Islamic Publications", + "year_of_publication": "2020", + "isbn": "978-1234567890", + "language": 1, + "main_themes": ["Hadith", "Islamic Jurisprudence", "Shia Islam"], + "notable_works": ["Volume 1: Faith and Disbelief", "Volume 2: Reason and Ignorance"], + "summary": "One of the most important Shia hadith collections", + "thumbnail": "https://example.com/media/books/alkafi.jpg", + "file_type": "pdf", + "book_file": "https://example.com/media/books/alkafi.pdf", + "view_count": 5432, + "download_count": 2456, + "pin": True, + "bookmark": False, + "user_rate": { + "is_rated": True, + "rate": 5 + }, + "average_rate": 4.7, + "created_at": "2024-01-15T10:30:00Z" } ] }, indent=2) @@ -576,12 +594,13 @@ class CustomAPIDocumentationView(View): 'name': 'Podcast Playlist List', 'method': 'GET', 'url': '/api/podcast/playlists/', - 'description': 'Get paginated list of podcast playlists', + 'description': 'Get paginated list of podcast playlists with filtering and sorting', 'parameters': [ - {'name': 'category', 'type': 'string', 'description': 'Filter by category slug', 'required': False}, + {'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False}, {'name': 'collection', 'type': 'string', 'description': 'Filter by collection slug', 'required': False}, {'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked playlists', 'required': False}, {'name': 'search', 'type': 'string', 'description': 'Search in playlist titles', 'required': False}, + {'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, title, -title, order, -order', 'required': False}, ], 'response_examples': { 'success': json.dumps({ @@ -595,6 +614,7 @@ class CustomAPIDocumentationView(View): "slogan": "Learn the basics of Islamic thought through audio", "view_count": 1234, "total_time_formatted": "02:45:30", + "episodes_count": 12, "order": 1, "created_at": "2024-01-15T10:30:00Z" } @@ -915,12 +935,13 @@ class CustomAPIDocumentationView(View): 'name': 'Article List', 'method': 'GET', 'url': '/api/article/list/', - 'description': 'Get paginated list of articles with filtering', + 'description': 'Get paginated list of articles with filtering and sorting', 'parameters': [ - {'name': 'category', 'type': 'string', 'description': 'Filter by category slug', 'required': False}, + {'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False}, {'name': 'collection', 'type': 'string', 'description': 'Filter by collection slug', 'required': False}, {'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked articles', 'required': False}, {'name': 'search', 'type': 'string', 'description': 'Search in article titles', 'required': False}, + {'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, title, -title', 'required': False}, ], 'response_examples': { 'success': json.dumps({ @@ -933,7 +954,21 @@ class CustomAPIDocumentationView(View): "thumbnail": "https://example.com/media/articles/thumb1.jpg", "description": "Краткая биография девятого имама", "view_count": 234, - "created_at": "2025-01-15T10:30:00Z" + "created_at": "2025-01-15T10:30:00Z", + "categories": [ + { + "id": 1, + "title": "Имамы", + "slug": "imams", + "acticle_count": 12 + }, + { + "id": 2, + "title": "Биография", + "slug": "biography", + "acticle_count": 8 + } + ] } ] }, indent=2) diff --git a/apps/article/serializers.py b/apps/article/serializers.py index 155559e..8a50354 100644 --- a/apps/article/serializers.py +++ b/apps/article/serializers.py @@ -43,10 +43,11 @@ class MiddleArticleCollectionSerializer(serializers.ModelSerializer): class ArticleListSerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() - + categories = ArticleCategoryListSerializer(many=True, read_only=True) + class Meta: model = Article - fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'view_count', 'created_at'] + fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'view_count', 'created_at', 'categories'] def get_thumbnail(self, obj): return get_thumbs(obj.thumbnail, self.context.get('request')) diff --git a/apps/article/views.py b/apps/article/views.py index f61fe17..02b4e43 100755 --- a/apps/article/views.py +++ b/apps/article/views.py @@ -81,40 +81,47 @@ class ArticleListAPIView(generics.ListAPIView): permission_classes = (IsAuthenticated,) @swagger_auto_schema( - operation_description="Get a list of article with optional filtering", + operation_description="Get a list of articles with optional filtering and sorting", manual_parameters=[ openapi.Parameter( name='category', in_=openapi.IN_QUERY, - description='Filter article by category slug', + description='Filter articles by category slug(s). Can be a single slug or comma-separated list of slugs', type=openapi.TYPE_STRING, required=False ), openapi.Parameter( name='collection', in_=openapi.IN_QUERY, - description='Filter article by collection slug', + description='Filter articles by collection slug', type=openapi.TYPE_STRING, required=False ), openapi.Parameter( name='is_bookmark', in_=openapi.IN_QUERY, - description='Filter article that are bookmarked by the user (true/false)', + description='Filter articles that are bookmarked by the user (true/false)', type=openapi.TYPE_BOOLEAN, required=False ), openapi.Parameter( name='search', in_=openapi.IN_QUERY, - description='Search article by title', + description='Search articles by title', + type=openapi.TYPE_STRING, + required=False + ), + openapi.Parameter( + name='sort', + in_=openapi.IN_QUERY, + description='Sort articles by field. Options: created_at, -created_at, view_count, -view_count, title, -title', type=openapi.TYPE_STRING, required=False ) ], responses={ 200: openapi.Response( - description="List of article", + description="List of articles", schema=ArticleListSerializer(many=True) ) } @@ -123,43 +130,54 @@ class ArticleListAPIView(generics.ListAPIView): return super().get(request, *args, **kwargs) def get_queryset(self): - queryset = Article.objects.filter(status=True).order_by('-created_at') - + queryset = Article.objects.filter(status=True) + # Search by title if search parameter is provided search_query = self.request.query_params.get('search', None) if search_query: queryset = queryset.filter(title__icontains=search_query) - + # Filter by category if provided - category_slug = self.request.query_params.get('category', None) - if category_slug: - queryset = queryset.filter(categories__slug=category_slug) - + category = self.request.query_params.get('category', None) + if category: + # Support both single slug and comma-separated list of slugs + category_slugs = [slug.strip() for slug in category.split(',')] + queryset = queryset.filter(categories__slug__in=category_slugs).distinct() + # Filter by collection if provided collection_slug = self.request.query_params.get('collection', None) if collection_slug: - # Get all podcasts that are in the collection with the given slug + # Get all articles that are in the collection with the given slug queryset = queryset.filter( collections__slug=collection_slug ) - - + + # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true': # Import Bookmark model here to avoid circular imports from apps.bookmark.models import Bookmark - - # Get all bookmarked podcast IDs for the current user + + # Get all bookmarked article IDs for the current user bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.ARTICLE, status=True ).values_list('content_id', flat=True) - - # Filter podcasts by these IDs + + # Filter articles by these IDs queryset = queryset.filter(id__in=bookmarked_ids) - + + # Sort by parameter + sort = self.request.query_params.get('sort', '-created_at') + # Allowed sort fields + allowed_sorts = ['created_at', '-created_at', 'view_count', '-view_count', 'title', '-title'] + if sort in allowed_sorts: + queryset = queryset.order_by(sort) + else: + queryset = queryset.order_by('-created_at') + return queryset diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 238357d..db8a1ad 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -15,7 +15,7 @@ class HadisListSerializer(serializers.ModelSerializer): class Meta: model = Hadis - fields = ['id', 'number', 'title', 'category', 'translation'] + fields = ['id', 'number', 'title', 'text' ,'category', 'translation'] def get_category(self, obj): """Get category id and title""" diff --git a/apps/library/doc.py b/apps/library/doc.py index 4995d99..84b62b2 100644 --- a/apps/library/doc.py +++ b/apps/library/doc.py @@ -41,10 +41,26 @@ is_bookmark_param = openapi.Parameter( required=False ) +category_param = openapi.Parameter( + 'category', + openapi.IN_QUERY, + description="Filter books by category slug(s). Can be a single slug or comma-separated list of slugs", + type=openapi.TYPE_STRING, + required=False +) + +sort_param = openapi.Parameter( + 'sort', + openapi.IN_QUERY, + description="Sort books by field. Options: created_at, -created_at, view_count, -view_count, download_count, -download_count, title, -title, pin, -pin, -pin,-created_at", + type=openapi.TYPE_STRING, + required=False +) + search_param = openapi.Parameter( 'search', openapi.IN_QUERY, - description="Search books by title, summary, or author", + description="Search books by title, summary, publisher, or isbn", type=openapi.TYPE_STRING, required=False ) @@ -82,6 +98,21 @@ book_schema = openapi.Schema( type=openapi.TYPE_STRING, description="Author of the book" ), + 'language': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Language ID of the book", + nullable=True + ), + 'main_themes': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema(type=openapi.TYPE_STRING), + description="List of main themes" + ), + 'notable_works': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema(type=openapi.TYPE_STRING), + description="List of notable works" + ), 'status': openapi.Schema( type=openapi.TYPE_BOOLEAN, description="Whether the book is active/visible" @@ -178,17 +209,23 @@ book_list_swagger = swagger_auto_schema( You can filter books by: - Collection ID using the query parameter 'collection_id' - - Middle section collection using the query parameter 'middle' - - Bottom section collection using the query parameter 'bottom' + - Category slug(s) using the query parameter 'category' (single slug or comma-separated list) - Bookmarked books using the query parameter 'is_bookmark=true' - You can also search for books by title, summary, or author using the query parameter 'search'. + You can also search for books by title, summary, publisher, or isbn using the query parameter 'search'. + + You can sort books by: + - created_at, -created_at + - view_count, -view_count + - download_count, -download_count + - title, -title + - pin, -pin, -pin,-created_at Note: To get downloaded books, use the separate endpoint /books/downloaded/ """, operation_summary="List Books", tags=["Library"], - manual_parameters=[collection_id_param, middle_param, bottom_param, is_bookmark_param, search_param], + manual_parameters=[collection_id_param, category_param, is_bookmark_param, search_param, sort_param], responses={ 200: books_response, 401: "Authentication credentials were not provided or are invalid.", diff --git a/apps/library/migrations/0007_auto_20251203_1529.py b/apps/library/migrations/0007_auto_20251203_1529.py new file mode 100644 index 0000000..673b452 --- /dev/null +++ b/apps/library/migrations/0007_auto_20251203_1529.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.8 on 2025-12-03 15:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0006_remove_book_author_book_isbn_book_numnber_of_volume_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='author', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apps/library/migrations/0008_auto_20251203_1533.py b/apps/library/migrations/0008_auto_20251203_1533.py new file mode 100644 index 0000000..532440e --- /dev/null +++ b/apps/library/migrations/0008_auto_20251203_1533.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.8 on 2025-12-03 15:33 + +from django.db import migrations, models +import dj_language.field + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0007_auto_20251203_1529'), + ('dj_language', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='language', + field=dj_language.field.LanguageField(blank=True, null=True, on_delete=models.SET_NULL, to='dj_language.language', verbose_name='Language'), + ), + migrations.AddField( + model_name='book', + name='main_themes', + field=models.JSONField(blank=True, default=list, help_text='List of main themes', verbose_name='Main Themes'), + ), + migrations.AddField( + model_name='book', + name='notable_works', + field=models.JSONField(blank=True, default=list, help_text='List of notable works', verbose_name='Notable Works'), + ), + ] diff --git a/apps/library/models.py b/apps/library/models.py index 1ebe42a..4476958 100644 --- a/apps/library/models.py +++ b/apps/library/models.py @@ -3,6 +3,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from filer.fields.image import FilerImageField +from dj_language.field import LanguageField from utils import generate_slug_for_model from apps.account.models import User @@ -108,10 +109,15 @@ class Book(models.Model): publisher = models.CharField(max_length=655, null=True, blank=True) year_of_publication = models.CharField(max_length=255, null=True, blank=True) - # author = models.CharField(max_length=255, null=True, blank=True) + author = models.CharField(max_length=255, null=True, blank=True) isbn = models.CharField(max_length=255, null=True, blank=True) numnber_of_volume = models.CharField(max_length=255, null=True, blank=True) - + + # Language, themes and notable works + language = LanguageField(verbose_name=_('Language'), null=True, blank=True) + main_themes = models.JSONField(verbose_name=_('Main Themes'), default=list, blank=True, help_text=_('List of main themes')) + notable_works = models.JSONField(verbose_name=_('Notable Works'), default=list, blank=True, help_text=_('List of notable works')) + pages_count = models.CharField(verbose_name=_('Number of Pages'), max_length=255, help_text=_('eg. 34'), null=True) status = models.BooleanField(default=True, verbose_name=_('status')) pin = models.BooleanField(default=True, verbose_name=_('Pin to top')) diff --git a/apps/library/serializers.py b/apps/library/serializers.py index 740e138..3bbd835 100644 --- a/apps/library/serializers.py +++ b/apps/library/serializers.py @@ -60,7 +60,8 @@ class BookSerializer(serializers.ModelSerializer): model = Book fields = ( 'id', 'title', 'slug', 'summary', 'summary_title', 'thumbnail', 'slogan', - 'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'isbn', 'numnber_of_volume', + 'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'author', 'isbn', 'numnber_of_volume', + 'language', 'main_themes', 'notable_works', 'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate', 'average_rate' ) diff --git a/apps/library/views.py b/apps/library/views.py index c415bea..c069d0c 100644 --- a/apps/library/views.py +++ b/apps/library/views.py @@ -103,6 +103,13 @@ class BookListView(ListAPIView): if collection_id: queryset = queryset.filter(collections__id=collection_id) + # Filter by category if provided + category = self.request.query_params.get('category') + if category: + # Support both single slug and comma-separated list of slugs + category_slugs = [slug.strip() for slug in category.split(',')] + queryset = queryset.filter(categories__slug__in=category_slugs).distinct() + # Filter by middle collection if requested # if self.request.query_params.get('middle'): # middle_collections = BookCollection.objects.filter( @@ -126,18 +133,35 @@ class BookListView(ListAPIView): if is_bookmark == 'true': # Import Bookmark model here to avoid circular imports from apps.bookmark.models import Bookmark - + # Get all bookmarked book IDs for the current user bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.LIBRARY, status=True ).values_list('content_id', flat=True) - + # Filter books by these IDs queryset = queryset.filter(id__in=bookmarked_ids) - return queryset.order_by('-pin', '-created_at') + # Sort by parameter + sort = self.request.query_params.get('sort', '-pin,-created_at') + # Allowed sort fields + allowed_sorts = [ + 'created_at', '-created_at', 'view_count', '-view_count', + 'download_count', '-download_count', 'title', '-title', + 'pin', '-pin', '-pin,-created_at' + ] + if sort in allowed_sorts: + # Handle multiple sort fields (e.g., '-pin,-created_at') + if ',' in sort: + queryset = queryset.order_by(*sort.split(',')) + else: + queryset = queryset.order_by(sort) + else: + queryset = queryset.order_by('-pin', '-created_at') + + return queryset class BookDetailView(RetrieveAPIView): diff --git a/apps/podcast/serializers.py b/apps/podcast/serializers.py index ea48d44..e50547a 100755 --- a/apps/podcast/serializers.py +++ b/apps/podcast/serializers.py @@ -188,14 +188,15 @@ class PinnedPodcastCollectionSerializer(serializers.ModelSerializer): class PodcastPlaylistListSerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() total_time_formatted = serializers.SerializerMethodField() - + episodes_count = serializers.SerializerMethodField() + class Meta: model = PodcastPlaylist - fields = ['id', 'title', 'slug', 'thumbnail', 'slogan', 'view_count', 'total_time_formatted', 'order', 'created_at'] + fields = ['id', 'title', 'slug', 'thumbnail', 'slogan', 'view_count', 'total_time_formatted', 'episodes_count', 'order', 'created_at'] def get_thumbnail(self, obj): return get_thumbs(obj.thumbnail, self.context.get('request')) - + def get_total_time_formatted(self, obj): """Format total_time as HH:MM:SS string""" if obj.total_time: @@ -206,6 +207,10 @@ class PodcastPlaylistListSerializer(serializers.ModelSerializer): return f"{hours:02d}:{minutes:02d}:{seconds:02d}" return "00:00:00" + def get_episodes_count(self, obj): + """Return the number of episodes (podcasts) in this playlist""" + return obj.playlist_items.count() + class PodcastPlaylistDetailSerializer(serializers.ModelSerializer): categories = PodcastCategoryListSerializer(many=True, read_only=True) diff --git a/apps/podcast/views.py b/apps/podcast/views.py index 7c48622..06d423f 100755 --- a/apps/podcast/views.py +++ b/apps/podcast/views.py @@ -90,12 +90,12 @@ class PodcastListAPIView(generics.ListAPIView): permission_classes = (IsAuthenticated,) @swagger_auto_schema( - operation_description="Get a list of podcast playlists with optional filtering", + operation_description="Get a list of podcast playlists with optional filtering and sorting", manual_parameters=[ openapi.Parameter( name='category', in_=openapi.IN_QUERY, - description='Filter playlists by category slug', + description='Filter playlists by category slug(s). Can be a single slug or comma-separated list of slugs', type=openapi.TYPE_STRING, required=False ), @@ -119,11 +119,18 @@ class PodcastListAPIView(generics.ListAPIView): description='Search playlists by title', type=openapi.TYPE_STRING, required=False + ), + openapi.Parameter( + name='sort', + in_=openapi.IN_QUERY, + description='Sort playlists by field. Options: created_at, -created_at, view_count, -view_count, title, -title, order, -order', + type=openapi.TYPE_STRING, + required=False ) ], responses={ 200: openapi.Response( - description="List of podcast playlists", + description="List of podcast playlists with episodes count", schema=PodcastPlaylistListSerializer(many=True) ) } @@ -132,38 +139,52 @@ class PodcastListAPIView(generics.ListAPIView): return super().get(request, *args, **kwargs) def get_queryset(self): - queryset = PodcastPlaylist.objects.filter(status=True).order_by('-created_at') - + queryset = PodcastPlaylist.objects.filter(status=True) + # Search by title if search parameter is provided search_query = self.request.query_params.get('search', None) if search_query: queryset = queryset.filter(title__icontains=search_query) - + # Filter by category if provided - category_slug = self.request.query_params.get('category', None) - if category_slug: - queryset = queryset.filter(categories__slug=category_slug) - + category = self.request.query_params.get('category', None) + if category: + # Support both single slug and comma-separated list of slugs + category_slugs = [slug.strip() for slug in category.split(',')] + queryset = queryset.filter(categories__slug__in=category_slugs).distinct() + # Filter by collection if provided collection_slug = self.request.query_params.get('collection', None) if collection_slug: queryset = queryset.filter( collections__slug=collection_slug ) - + # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true': from apps.bookmark.models import Bookmark - + bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.PODCAST_PLAYLIST, status=True ).values_list('content_id', flat=True) - + queryset = queryset.filter(id__in=bookmarked_ids) - + + # Sort by parameter + sort = self.request.query_params.get('sort', '-created_at') + # Allowed sort fields + allowed_sorts = [ + 'created_at', '-created_at', 'view_count', '-view_count', + 'title', '-title', 'order', '-order' + ] + if sort in allowed_sorts: + queryset = queryset.order_by(sort) + else: + queryset = queryset.order_by('-created_at') + return queryset