Index: backend/subjects/migrations/0004_evaluationreview_evaluationmethod_review_otherreview_and_more.py
===================================================================
--- backend/subjects/migrations/0004_evaluationreview_evaluationmethod_review_otherreview_and_more.py	(revision 10c6536b0fa848158c1516afea1c8265cb1e1b18)
+++ backend/subjects/migrations/0004_evaluationreview_evaluationmethod_review_otherreview_and_more.py	(revision 10c6536b0fa848158c1516afea1c8265cb1e1b18)
@@ -0,0 +1,68 @@
+# Generated by Django 5.1.7 on 2025-07-12 11:56
+
+import django.core.validators
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth_form', '0018_student_disliked_subjects_student_liked_subjects'),
+        ('subjects', '0003_remove_subject_info_short'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='EvaluationReview',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='EvaluationMethod',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('note', models.CharField(blank=True, max_length=64, null=True)),
+                ('evaluation_review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subjects.evaluationreview')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Review',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('is_confirmed', models.BooleanField(default=False, help_text='Has an admin confirmed this post is valid.')),
+                ('upvotes', models.IntegerField(default=0)),
+                ('downvotes', models.IntegerField(default=0)),
+                ('review_type', models.CharField(choices=[('evaluation', 'Evaluation'), ('other', 'Other')], max_length=16)),
+                ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth_form.student')),
+                ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subjects.subject')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='OtherReview',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('content', models.TextField()),
+                ('category', models.CharField(choices=[('material', 'Material'), ('staff', 'Staff'), ('other', 'Other')], max_length=16)),
+                ('review', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='subjects.review')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='evaluationreview',
+            name='review',
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='subjects.review'),
+        ),
+        migrations.CreateModel(
+            name='EvaluationComponent',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('category', models.CharField(choices=[('project', 'Project'), ('theory', 'Theory'), ('practical', 'Practical'), ('homework', 'Homework'), ('labs', 'Labs'), ('presentation', 'Presentation'), ('attendance', 'Attendance')], max_length=16)),
+                ('percentage', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
+                ('evaluation_method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subjects.evaluationmethod')),
+            ],
+            options={
+                'constraints': [models.UniqueConstraint(fields=('evaluation_method', 'category'), name='unique_component_per_method')],
+            },
+        ),
+    ]
Index: backend/subjects/migrations/0005_remove_review_downvotes_remove_review_upvotes_and_more.py
===================================================================
--- backend/subjects/migrations/0005_remove_review_downvotes_remove_review_upvotes_and_more.py	(revision 10c6536b0fa848158c1516afea1c8265cb1e1b18)
+++ backend/subjects/migrations/0005_remove_review_downvotes_remove_review_upvotes_and_more.py	(revision 10c6536b0fa848158c1516afea1c8265cb1e1b18)
@@ -0,0 +1,41 @@
+# Generated by Django 5.1.7 on 2025-07-12 12:27
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth_form', '0018_student_disliked_subjects_student_liked_subjects'),
+        ('subjects', '0004_evaluationreview_evaluationmethod_review_otherreview_and_more'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='review',
+            name='downvotes',
+        ),
+        migrations.RemoveField(
+            model_name='review',
+            name='upvotes',
+        ),
+        migrations.CreateModel(
+            name='ReviewVote',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('vote_type', models.CharField(choices=[('up', 'Upvote'), ('down', 'Downvote')], max_length=4)),
+                ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subjects.review')),
+                ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth_form.student')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='review',
+            name='votes',
+            field=models.ManyToManyField(related_name='review_votes', through='subjects.ReviewVote', to='auth_form.student'),
+        ),
+        migrations.AddConstraint(
+            model_name='reviewvote',
+            constraint=models.UniqueConstraint(fields=('review', 'student'), name='unique_review_per_student'),
+        ),
+    ]
Index: backend/subjects/models.py
===================================================================
--- backend/subjects/models.py	(revision de9612a2177507c9a6e8cfc9d172ebb47edae002)
+++ backend/subjects/models.py	(revision 10c6536b0fa848158c1516afea1c8265cb1e1b18)
@@ -1,4 +1,7 @@
+from django.core.validators import MinValueValidator, MaxValueValidator
 from django.db import models
 from django.contrib.postgres.fields import ArrayField
+from auth_form.models import Student
+
 
 class Subject(models.Model):
@@ -41,2 +44,94 @@
     class Meta:
         db_table = 'subject_info'
+
+
+class Review(models.Model):
+    REVIEW_TYPE_CHOICES = [
+        ("evaluation", "Evaluation"),
+        ("other", "Other"),
+    ]
+    # each review is written by one student
+    # one student can write many reviews
+    student = models.ForeignKey(Student, on_delete=models.CASCADE)
+    # each review is about one subject
+    # one subject can have many reviews
+    subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
+    is_confirmed = models.BooleanField(default=False, help_text="Has an admin confirmed this post is valid.")
+    votes = models.ManyToManyField(
+        Student, through='ReviewVote', related_name='review_votes'
+    )
+    review_type = models.CharField(max_length=16, choices=REVIEW_TYPE_CHOICES)
+
+    def __str__(self):
+        return f"Review for {self.subject.name} from {self.student.index}."
+
+    @property
+    def upvote_count(self):
+        return self.votes.filter(review_votes__reviewvote__vote_type='up').count()
+    @property
+    def downvote_count(self):
+        return self.votes.filter(review_votes__reviewvote__vote_type='down').count()
+
+class ReviewVote(models.Model):
+    VOTE_TYPES = [
+        ('up', 'Upvote'),
+        ('down', 'Downvote'),
+    ]
+    review = models.ForeignKey(Review, on_delete=models.CASCADE)
+    student = models.ForeignKey(Student, on_delete=models.CASCADE)
+    vote_type = models.CharField(max_length=4, choices=VOTE_TYPES)
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(fields=['review', 'student'], name='unique_review_per_student')
+        ]
+
+class EvaluationReview(models.Model):
+    review = models.OneToOneField(Review, on_delete=models.CASCADE)
+
+class EvaluationMethod(models.Model):
+    # one evaluation review could have more evaluation methods
+    # example through a project and through exams
+    # option A: project: 90%, labs: 10%
+    # option B: theory: 35%, practical: 35%, labs: 10%, project: 20%
+    # each of these (option A - project, option A - labs, option B - project etc. is a EvaluationComponent)
+    evaluation_review = models.ForeignKey(EvaluationReview, on_delete=models.CASCADE)
+    note = models.CharField(max_length=64, null=True, blank=True, help_text="additional info about this particular evaluation method.")
+
+class EvaluationComponent(models.Model):
+    CATEGORY_TYPE_CHOICES = [
+        ("project", "Project"),
+        ("theory", "Theory"),
+        ("practical", "Practical"),
+        ("homework", "Homework"),
+        ("labs", "Labs"),
+        ("presentation", "Presentation"),
+        ("attendance", "Attendance"),
+        # todo: check if there are more
+    ]
+    evaluation_method = models.ForeignKey(EvaluationMethod, on_delete=models.CASCADE)
+    category = models.CharField(max_length=16, choices=CATEGORY_TYPE_CHOICES)
+    percentage = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(100)])
+
+    def __str__(self):
+        subject = self.evaluation_method.evaluation_review.review.subject.name
+        return f"{self.percentage}% of the grade for {subject} is from {self.category}"
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(fields=['evaluation_method', 'category'], name='unique_component_per_method')
+        ]
+
+class OtherReview(models.Model):
+    CATEGORY_TYPE_CHOICES = [
+        ("material", "Material"),
+        ("staff", "Staff"),
+        ("other", "Other"),
+    ]
+    review = models.OneToOneField(Review, on_delete=models.CASCADE)
+    content = models.TextField()
+    category = models.CharField(max_length=16, choices=CATEGORY_TYPE_CHOICES)
+
+    def __str__(self):
+        return f"Review for {self.category} about {self.review.subject.name}."
+
