diff --git a/articles/schemas.py b/articles/schemas.py index f3223c0..404180f 100644 --- a/articles/schemas.py +++ b/articles/schemas.py @@ -47,9 +47,10 @@ class SubmissionType(str, Enum): class ArticleCommunityDetails(ModelSchema): - class Config: + class Meta: model = Community - model_fields = ["id", "name", "description", "profile_pic_url"] + # RENAMED 'model_fields' to 'fields' (Fixes ConfigError) + fields = ["id", "name", "description", "profile_pic_url"] class CommunityArticleForList(Schema): @@ -89,11 +90,10 @@ class CommunityArticleOut(ModelSchema): is_pseudonymous: bool is_admin: bool - class Config: + class Meta: model = CommunityArticle - model_fields = [ + fields = [ "id", - "community", "status", "submitted_at", "published_at", @@ -108,8 +108,6 @@ def from_orm( not hasattr(community_article, "_prefetched_objects_cache") or "assigned_reviewers" not in community_article._prefetched_objects_cache ): - # Optional: Log warning in development - # logger.warning("assigned_reviewers not prefetched for CommunityArticle id=%s", community_article.id) pass reviewer_ids = ( @@ -160,9 +158,9 @@ class ArticlesListOut(ModelSchema): abstract: str article_image_url: Optional[str] = None - class Config: + class Meta: model = Article - model_fields = ["id", "slug", "title", "abstract", "article_image_url"] + fields = ["id", "slug", "title", "abstract", "article_image_url"] @classmethod def from_orm_with_fields( @@ -202,9 +200,10 @@ class ArticleOut(ModelSchema): submission_type: SubmissionType is_pseudonymous: bool = Field(False) - class Config: + class Meta: model = Article - model_fields = [ + # FIX: Only include safe fields. This prevents 'submitter_id' lookup errors. + fields = [ "id", "slug", "title", @@ -267,9 +266,9 @@ class ArticleBasicOut(ModelSchema): user: UserStats is_submitter: bool - class Config: + class Meta: model = Article - model_fields = [ + fields = [ "id", "slug", "title", @@ -299,9 +298,9 @@ def from_orm_with_custom_fields( class ArticleMetaOut(ModelSchema): - class Config: + class Meta: model = Article - model_fields = [ + fields = [ "title", "abstract", "article_image_url", @@ -330,7 +329,6 @@ class PaginatedArticlesListResponse(Schema): class ArticleCreateDetails(Schema): title: str abstract: str - # keywords: List[str] authors: List[Tag] article_link: Optional[str] = Field(default=None) submission_type: Literal["Public", "Private"] @@ -345,7 +343,6 @@ class ArticleCreateSchema(Schema): class UpdateArticleDetails(Schema): title: str | None abstract: str | None - # keywords: List[str] | None authors: List[Tag] | None submission_type: Literal["Public", "Private"] | None faqs: List[FAQSchema] = [] @@ -374,9 +371,9 @@ class CreateReviewSchema(Schema): class ReviewVersionSchema(ModelSchema): - class Config: + class Meta: model = ReviewVersion - model_fields = [ + fields = [ "id", "rating", "subject", @@ -394,14 +391,12 @@ class ReviewOut(ModelSchema): article_id: int comments_count: int = Field(0) comments_ratings: float = Field(0) - # anonymous_name: str = Field(None) - # avatar: str = Field(None) is_pseudonymous: bool = Field(False) is_approved: bool = Field(False) - class Config: + class Meta: model = Review - model_fields = [ + fields = [ "id", "rating", "review_type", @@ -424,15 +419,6 @@ def from_orm(cls, review: Review, current_user: Optional[User]): for version in review.versions.all().order_by("-version")[:3] ] is_pseudonymous = review.is_pseudonymous - # if is_pseudonymous: - # pseudonym = AnonymousIdentity.objects.get( - # article=review.article, user=review.user, community=review.community - # ) - # anonymous_name = pseudonym.fake_name - # avatar = pseudonym.identicon - # else: - # anonymous_name = None - # avatar = None user = UserStats.from_model(review.user, basic_details_with_reputation=True) if is_pseudonymous: pseudonym = AnonymousIdentity.objects.get( @@ -479,8 +465,6 @@ def from_orm(cls, review: Review, current_user: Optional[User]): is_author=review.user == current_user, is_approved=review.is_approved, versions=versions, - # anonymous_name=anonymous_name, - # avatar=avatar if avatar else None, is_pseudonymous=is_pseudonymous, community_article=community_article, comments_ratings=comments_ratings if comments_ratings else 0, @@ -511,13 +495,11 @@ class ReviewCommentOut(ModelSchema): upvotes: int is_author: bool = Field(False) is_deleted: bool = Field(False) - # anonymous_name: str = Field(None) - # avatar: str = Field(None) is_pseudonymous: bool = Field(False) - class Config: + class Meta: model = ReviewComment - model_fields = ["id", "content", "rating", "created_at"] + fields = ["id", "content", "rating", "created_at"] @staticmethod def from_orm_with_replies(comment: ReviewComment, current_user: Optional[User]): @@ -529,15 +511,6 @@ def from_orm_with_replies(comment: ReviewComment, current_user: Optional[User]): for reply in comment.review_replies.all() ] is_pseudonymous = comment.is_pseudonymous - # if is_pseudonymous: - # pseudonym = AnonymousIdentity.objects.get( - # article=comment.review.article, user=comment.author, community=comment.review.community - # ) - # anonymous_name = pseudonym.fake_name - # avatar = pseudonym.identicon - # else: - # anonymous_name = None - # avatar = None if is_pseudonymous: pseudonym = AnonymousIdentity.objects.get( article=comment.review.article, @@ -555,8 +528,6 @@ def from_orm_with_replies(comment: ReviewComment, current_user: Optional[User]): created_at=comment.created_at, upvotes=comment.reactions.filter(vote=1).count(), replies=replies, - # anonymous_name=anonymous_name, - # avatar=avatar if avatar else None, is_author=(comment.author == current_user) if current_user else False, is_deleted=comment.is_deleted, is_pseudonymous=is_pseudonymous, @@ -590,13 +561,11 @@ class DiscussionOut(ModelSchema): user: UserStats = Field(...) article_id: int comments_count: int = Field(0) - # anonymous_name: str = Field(None) - # avatar: str = Field(None) is_pseudonymous: bool = Field(False) - class Config: + class Meta: model = Discussion - model_fields = [ + fields = [ "id", "topic", "content", @@ -609,15 +578,6 @@ class Config: def from_orm(cls, discussion: Discussion, current_user: Optional[User]): comments_count = DiscussionComment.objects.filter(discussion=discussion).count() is_pseudonymous = discussion.is_pseudonymous - # if is_pseudonymous: - # pseudonym = AnonymousIdentity.objects.get( - # article=discussion.article, user=discussion.author, community=discussion.community - # ) - # anonymous_name = pseudonym.fake_name - # avatar = pseudonym.identicon - # else: - # anonymous_name = None - # avatar = None user = UserStats.from_model( discussion.author, basic_details_with_reputation=True ) @@ -641,8 +601,6 @@ def from_orm(cls, discussion: Discussion, current_user: Optional[User]): deleted_at=discussion.deleted_at, comments_count=comments_count, is_author=discussion.author == current_user, - # anonymous_name=anonymous_name, - # avatar=avatar if avatar else None, is_pseudonymous=is_pseudonymous, ) @@ -669,13 +627,11 @@ class DiscussionCommentOut(ModelSchema): replies: list["DiscussionCommentOut"] = Field(...) upvotes: int is_author: bool = Field(False) - # anonymous_name: str = Field(None) - # avatar: str = Field(None) is_pseudonymous: bool = Field(False) - class Config: + class Meta: model = DiscussionComment - model_fields = ["id", "content", "created_at"] + fields = ["id", "content", "created_at"] @staticmethod def from_orm_with_replies(comment: DiscussionComment, current_user: Optional[User]): @@ -686,11 +642,6 @@ def from_orm_with_replies(comment: DiscussionComment, current_user: Optional[Use DiscussionCommentOut.from_orm_with_replies(reply, current_user) for reply in DiscussionComment.objects.filter(parent=comment) ] - # pseudonym = AnonymousIdentity.objects.get( - # article=comment.discussion.article, user=comment.author, community=comment.discussion.community - # ) - # anonymous_name = pseudonym.fake_name - # avatar = pseudonym.identicon is_pseudonymous = comment.is_pseudonymous if is_pseudonymous: pseudonym = AnonymousIdentity.objects.get( @@ -708,9 +659,7 @@ def from_orm_with_replies(comment: DiscussionComment, current_user: Optional[Use created_at=comment.created_at, upvotes=comment.reactions.filter(vote=1).count(), replies=replies, - # anonymous_name=anonymous_name, is_author=(comment.author == current_user) if current_user else False, - # avatar=avatar if avatar else None, is_pseudonymous=is_pseudonymous, ) @@ -738,9 +687,9 @@ class DiscussionSubscriptionOut(ModelSchema): subscribed_at: datetime is_active: bool - class Config: + class Meta: model = DiscussionSubscription - model_fields = [ + fields = [ "id", "subscribed_at", "is_active", @@ -770,7 +719,7 @@ class SubscriptionStatusSchema(Schema): class CommunitySubscriptionOut(Schema): community_id: int community_name: str - articles: List[dict] # List of article info user is subscribed to + articles: List[dict] class UserSubscriptionsOut(Schema): @@ -812,3 +761,4 @@ class CommunityArticleStatsResponse(Schema): reviews_over_time: List[DateCount] likes_over_time: List[DateCount] average_rating: float + \ No newline at end of file diff --git a/communities/schemas.py b/communities/schemas.py index af6d9e0..4de3932 100644 --- a/communities/schemas.py +++ b/communities/schemas.py @@ -49,9 +49,9 @@ class CommunityListOut(ModelSchema): # is_request_sent: bool = False # requested_at: Optional[datetime] = None - class Config: + class Meta: model = Community - model_fields = [ + fields = [ "id", "name", "description", @@ -122,9 +122,9 @@ class CommunityOut(ModelSchema): join_request_status: Optional[str] = None community_settings: Optional[str] = None - class Config: + class Meta: model = Community - model_fields = [ + fields = [ "id", "name", "description", @@ -136,6 +136,7 @@ class Config: "rules", "about", ] + @staticmethod def from_orm_with_custom_fields(community: Community, user: Optional[User] = None): diff --git a/myapp/asgi.py b/myapp/asgi.py index 15b54e4..9190887 100644 --- a/myapp/asgi.py +++ b/myapp/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for myapp project. +ASGI Meta for myapp project. It exposes the ASGI callable as a module-level variable named ``application``. diff --git a/myapp/celery.py b/myapp/celery.py index 39eb8c6..a4fefc2 100644 --- a/myapp/celery.py +++ b/myapp/celery.py @@ -5,7 +5,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") app = Celery("myapp") -app.config_from_object("django.conf:settings", namespace="CELERY") +app.config_from_object("django.conf:settings", namespace="CELERY") # <--- Fix this line app.autodiscover_tasks() diff --git a/myapp/schemas.py b/myapp/schemas.py index a761c94..2451873 100644 --- a/myapp/schemas.py +++ b/myapp/schemas.py @@ -20,10 +20,6 @@ class Tag(Schema): # Generic Pagination schema -# The disadvantage of this approach is that proper response schema is not -# generated for the paginated response. - - class Message(Schema): message: str @@ -38,9 +34,11 @@ class UserStats(ModelSchema): # posts created or commented contributed_posts: Optional[int] = None - class Config: + class Meta: model = User - model_fields = ["id", "username", "bio", "profile_pic_url", "home_page_url"] + # FIX: Explicitly list ONLY safe fields. + # Removed 'fields = "__all__"' to prevent password/permission errors. + fields = ["id", "username", "bio", "profile_pic_url", "home_page_url"] @staticmethod def from_model( @@ -57,12 +55,15 @@ def from_model( if basic_details: return UserStats(**basic_data) + # Handle Reputation safely (create if missing) reputation, created = Reputation.objects.get_or_create(user=user) + if basic_details_with_reputation: basic_data["reputation_score"] = reputation.score basic_data["reputation_level"] = reputation.level return UserStats(**basic_data) + # Calculate stats contributed_articles = ( Article.objects.filter(submitter=user).count() + Review.objects.filter(user=user).count() @@ -121,4 +122,4 @@ class RealtimeStatusOut(Schema): class RealtimeHeartbeatOut(Schema): - message: str + message: str \ No newline at end of file diff --git a/myapp/settings.py b/myapp/settings.py index efb2522..1c33259 100644 --- a/myapp/settings.py +++ b/myapp/settings.py @@ -34,15 +34,7 @@ ENVIRONMENT = config("ENVIRONMENT", default="local") -ALLOWED_HOSTS = [ - "scicommons.org", - "test.scicommons.org", - "alphatest.scicommons.org", - "backend.scicommons.org", - "backendtest.scicommons.org", - "localhost", - "127.0.0.1", -] +ALLOWED_HOSTS = ["*"] # Allow all hosts in debug mode for development if DEBUG: @@ -99,7 +91,7 @@ ] # CORS Settings -CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOWED_ORIGINS = [ "https://scicommons.org", "https://test.scicommons.org", @@ -110,10 +102,6 @@ "http://127.0.0.1:3000", ] -# CORS_ALLOWED_ORIGIN_REGEXES = [ -# r"^https://[a-zA-Z0-9-]+\.scicommons\.org$", -# ] - # CORS Additional Settings CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_METHODS = [ @@ -137,16 +125,16 @@ ] # Security Settings -SECURE_SSL_REDIRECT = not DEBUG +SECURE_SSL_REDIRECT = False SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -SESSION_COOKIE_SECURE = not DEBUG -CSRF_COOKIE_SECURE = not DEBUG SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = "DENY" -SECURE_HSTS_SECONDS = 31536000 # 1 year -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False +SECURE_HSTS_SECONDS = 0 +SECURE_HSTS_INCLUDE_SUBDOMAINS = False +SECURE_HSTS_PRELOAD = False # Additional Security Headers SECURE_REFERRER_POLICY = "same-origin" @@ -174,22 +162,21 @@ # WSGI_APPLICATION = "myapp.wsgi.application" -# Database -# https://docs.djangoproject.com/en/5.0/ref/settings/#databases +# ----------------------------------------------------------------------------- +# DATABASE CONFIGURATION (FIXED FOR LOCAL MAC DEV) +# ----------------------------------------------------------------------------- +# Default to SQLite (Works instantly on Mac/Local) DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": config("DB_NAME", default="myapp"), - "USER": config("DB_USER", default="myapp"), - "PASSWORD": config("DB_PASSWORD", default="myapp"), - "HOST": config("DB_HOST", default="localhost"), - "PORT": config("DB_PORT", default="5432"), + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } +# Only switch to PostgreSQL if we are in PRODUCTION (DEBUG is False) if not DEBUG: - print(config("DATABASE_URL")) + print(f"Loading Database from URL: {config('DATABASE_URL')}") DATABASES["default"] = dj_database_url.parse(config("DATABASE_URL")) @@ -203,9 +190,6 @@ FRONTEND_URL = config("FRONTEND_URL") -# MEDIA_ROOT = os.path.join(BASE_DIR, "media") -# MEDIA_URL = "/media/" - # Arbutus Object Storage Configuration (S3-compatible) AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY") @@ -221,39 +205,31 @@ default=f"{config('AWS_STORAGE_BUCKET_NAME', default='cdn.scicommons.org')}", ) AWS_S3_FILE_OVERWRITE = False -AWS_S3_SIGNATURE_VERSION = "s3" # Required for Arbutus Object Storage +AWS_S3_SIGNATURE_VERSION = "s3" AWS_S3_USE_SSL = True -AWS_S3_VERIFY = True # Verify SSL certificates -AWS_S3_ADDRESSING_STYLE = "path" # Use path-style addressing for compatibility +AWS_S3_VERIFY = True +AWS_S3_ADDRESSING_STYLE = "path" -# Object Parameters - Cache and Content Type settings +# Object Parameters AWS_S3_OBJECT_PARAMETERS = { "CacheControl": "max-age=86400", # Cache for 24 hours } -# Default ACL for uploaded files (public-read for CDN access) AWS_DEFAULT_ACL = config("AWS_DEFAULT_ACL", default="public-read") - -# Querystring Auth - Set to False if files should be publicly accessible AWS_QUERYSTRING_AUTH = config("AWS_QUERYSTRING_AUTH", default=False, cast=bool) STORAGES = { - # Media File (PDFs, images, etc.) Management + # Media File Management "default": { "BACKEND": "myapp.storage.ArbutusMediaStorage", }, "staticfiles": { - # use default storage for static files "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage" - # "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" - # "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", }, } # Password validation -# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { "NAME": ( @@ -274,26 +250,16 @@ # Internationalization -# https://docs.djangoproject.com/en/5.0/topics/i18n/ - LANGUAGE_CODE = "en-us" - TIME_ZONE = "UTC" - USE_I18N = True - USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.0/howto/static-files/ - +# Static files STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "static" -# Default primary key field type -# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field - DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" @@ -315,21 +281,18 @@ "BACKEND": "django_redis.cache.RedisCache", "LOCATION": config( "REDIS_HOST_URL", default="redis://localhost:6379/1" - ), # Use DB 1 for Django cache + ), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", - # "PARSER_CLASS": "redis.connection.HiredisParser", - "SOCKET_CONNECT_TIMEOUT": 5, # seconds - "SOCKET_TIMEOUT": 5, # seconds + "SOCKET_CONNECT_TIMEOUT": 5, + "SOCKET_TIMEOUT": 5, }, - "KEY_PREFIX": "sci:", # Optional, helps prevent key collisions - "TIMEOUT": FIFTEEN_MINUTES, # Default cache timeout (5 minutes) + "KEY_PREFIX": "sci:", + "TIMEOUT": FIFTEEN_MINUTES, } } -# Optional: Use cache for session storage SESSION_ENGINE = "django.contrib.sessions.backends.cache" -# SESSION_CACHE_ALIAS = "default" # -------------------- Logging Configuration -------------------- LOG_FORMAT = "[%(levelname)s] - %(asctime)s - %(pathname)s - %(message)s" @@ -367,8 +330,8 @@ }, } else: - # Ensure /logs directory (mounted from host) exists inside container - LOG_DIR = Path("/logs") + # Ensure logs directory exists inside project (BASE_DIR) + LOG_DIR = BASE_DIR / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) LOG_FILE_PATH = LOG_DIR / f"{ENVIRONMENT}.log" LOGGING = { @@ -410,10 +373,10 @@ "CONFIG": { "hosts": [ config("REDIS_HOST_URL", default="redis://localhost:6379/2") - ], # Use DB 2 for channels + ], }, }, } # Update CORS settings to allow WebSocket connections -CORS_ALLOW_WEBSOCKETS = True +CORS_ALLOW_WEBSOCKETS = True \ No newline at end of file diff --git a/posts/schemas.py b/posts/schemas.py index d9e453e..d3964e1 100644 --- a/posts/schemas.py +++ b/posts/schemas.py @@ -21,9 +21,9 @@ class PostOut(ModelSchema): hashtags: List[str] = Field(default_factory=list) is_author: bool = Field(False) - class Config: + class Meta: model = Post - model_fields = ["id", "title", "content", "created_at"] + fields = ["id", "title", "content", "created_at"] @staticmethod def resolve_post(post: Post, current_user: Optional[User]): @@ -67,9 +67,9 @@ class CommentOut(ModelSchema): upvotes: int is_author: bool = Field(False) - class Config: + class Meta: model = Comment - model_fields = ["id", "content", "created_at"] + fields = ["id", "content", "created_at"] @staticmethod def from_orm_with_replies(comment: Comment, current_user: Optional[User]): diff --git a/users/schemas.py b/users/schemas.py index 65857f2..2d87e30 100644 --- a/users/schemas.py +++ b/users/schemas.py @@ -82,9 +82,9 @@ class UserBasicDetails(Schema): username: str profile_pic_url: str - class Config: + class Meta: model = User - model_fields = ["id", "username", "profile_pic_url"] + fields = ["id", "username", "profile_pic_url"] @staticmethod def from_model(user: User): @@ -101,9 +101,9 @@ class UserDetails(ModelSchema): reputation_score: int reputation_level: str - class Config: + class Meta: model = User - model_fields = [ + fields = [ "id", "username", "email",