<file_sep>FROM postgres:latest

LABEL maintainer="<NAME>"
<file_sep># Generated by Django 3.0.7 on 2021-01-13 16:54

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('chronicles_backend', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='bugreport',
            name='creation',
            field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Timestamp of bug report'),
        ),
        migrations.AlterField(
            model_name='comment',
            name='creation',
            field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Timestamp of comment'),
        ),
        migrations.AlterField(
            model_name='project',
            name='creation',
            field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Timestamp of project creation'),
        ),
    ]
<file_sep>aioredis==1.3.1
appdirs==1.4.3
asgiref==3.2.7
async-timeout==3.0.1
attrs==19.3.0
autobahn==20.12.3
Automat==20.2.0
CacheControl==0.12.6
certifi==2020.4.5.1
cffi==1.14.0
channels==2.4.0
channels-redis==2.4.2
chardet==3.0.4
colorama==0.4.3
constantly==15.1.0
contextlib2==0.6.0
cryptography==3.3.2
daphne==2.5.0
distlib==0.3.0
distro==1.4.0
Django==3.1.12
django-cors-headers==3.2.1
django-environ==0.4.5
django-model-utils==4.0.0
djangorestframework==3.11.2
gunicorn==20.0.4
hiredis==1.0.1
html5lib==1.0.1
hyperlink==19.0.0
idna==2.9
incremental==17.5.0
ipaddr==2.2.0
lockfile==0.12.2
msgpack==0.6.2
packaging==20.3
pep517==0.8.2
Pillow==8.2.0
progress==1.5
psycopg2==2.8.6
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.20
PyHamcrest==2.0.2
pyOpenSSL==19.1.0
pyparsing==2.4.6
python-decouple==3.3
pytoml==0.1.21
pytz==2020.1
PyYAML==5.4
requests==2.23.0
retrying==1.3.3
service-identity==18.1.0
six==1.15.0
sqlparse==0.3.1
Twisted==20.3.0
txaio==20.4.1
urllib3==1.26.5
webencodings==0.5.1
zope.interface==5.1.0
<file_sep>import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import WebsocketConsumer
from channels.auth import get_user
from .models import BugReport, Comment
from .serializers import CommentVerboseSerializer


class CommentConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bug_id = self.scope['url_route']['kwargs']['pk']

    def connect(self):
        self.user = async_to_sync(get_user)(self.scope)
        if self.user.is_authenticated:
            try:
                bug_report = BugReport.objects.get(pk=self.bug_id)
                async_to_sync(self.channel_layer.group_add)(
                    self.bug_id,
                    self.channel_name
                )
                self.accept()
            except BugReport.DoesNotExist:
                self.close()
        else:
            self.close()

    def disconnect(self, code):
        async_to_sync(self.channel_layer.group_discard)(
            self.bug_id,
            self.channel_name
        )

    def receive(self, text_data=None, bytes_data=None):
        text_data_json = json.loads(text_data)
        comment_id = text_data_json['comment_id']
        try:
            comment = Comment.objects.get(pk=comment_id)
            comment_serializer = CommentVerboseSerializer(comment)
            async_to_sync(self.channel_layer.group_send)(
                self.bug_id,
                {
                    'type': 'send_comment',
                    'comment': comment_serializer.data,
                }
            )
        except Comment.DoesNotExist:
            pass

    def send_comment(self, event):
        comment = event['comment']
        self.send(text_data=json.dumps(comment))
<file_sep>from rest_framework import serializers
from chronicles_backend.models import *


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChronicleUser
        fields = ['id', 'username', 'first_name', 'last_name', 'isAdmin', 'is_active']


class ProjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = ['id', 'name', 'description', 'creator', 'team', 'creation', 'image', 'slug']
        read_only_fields = ['creator', 'creation']
        lookup_field = 'slug'
        extra_kwargs = {
            'url': {'lookup_field': 'slug'}
        }


class BugReportSerializer(serializers.ModelSerializer):
    class Meta:
        model = BugReport
        fields = ['id', 'project', 'reporter', 'heading', 'description',
                  'person_in_charge', 'creation', 'status', 'tagsHash']
        read_only_fields = ['reporter', 'creation', 'status', 'person_in_charge']


class BugReportEditSerializer(serializers.ModelSerializer):
    class Meta:
        model = BugReport
        fields = ['id', 'project', 'reporter', 'heading', 'description',
                  'person_in_charge', 'creation', 'status', 'tagsHash']
        read_only_fields = ['reporter', 'creation', 'project', 'heading', 'description', 'tagsHash']


class BugReportVerboseSerializer(serializers.ModelSerializer):
    reporter = UserSerializer()
    person_in_charge = UserSerializer()
    project = ProjectSerializer()

    class Meta:
        model = BugReport
        fields = ['id', 'project', 'reporter', 'heading', 'description',
                  'person_in_charge', 'creation', 'status', 'tagsHash']
        read_only_fields = ['reporter', 'creation', 'project', 'heading', 'description', 'tagsHash']


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'report', 'creation', 'commenter', 'body']
        read_only_fields = ['commenter', 'creation']


class CommentEditSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'report', 'creation', 'commenter', 'body']
        read_only_fields = ['commenter', 'creation', 'report']


class CommentVerboseSerializer(serializers.ModelSerializer):
    commenter = UserSerializer()

    class Meta:
        model = Comment
        fields = ['id', 'report', 'creation', 'commenter', 'body']
        read_only_fields = ['commenter', 'creation']


class ImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Image
        fields = ['id', 'randIdentifier', 'url']

# Tags legend:
# 0 :Functionality
# 1 :Usability
# 2 :Interface
# 3 :Compatibility
# 4 :Performance
# 5 :Security
# 6 :Management
# 7 :Code
# 8 :UI
# 9 :UX
# 10:New Feature
# 11:Fat gaya
# 12:Design
# 13:Front end
# 14:Back end
# 15:Database
# 16:External Resource
# 17:Coverage
# 18:Bug
# 19:Improvement
# 20:Broken
# 21:Cookie
# 22:Typo
# 23:
<file_sep>FROM python:3.8.5-alpine

LABEL maintainer="<NAME>"

ENV PYTHONUNBUFFERED 1

RUN apk update && apk add postgresql-dev gcc python3-dev musl-dev libffi-dev openssl-dev jpeg-dev zlib-dev libjpeg

RUN mkdir /code

WORKDIR /code

COPY requirements.txt /code/

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY . .
<file_sep>from django.db import models
from django.contrib.auth.models import AbstractUser
from datetime import datetime


class ChronicleUser(AbstractUser):
    enrNo = models.IntegerField(default=0)
    isAdmin = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)

    class Meta:
        ordering = ['enrNo']

    def __str__(self):
        return self.get_username()


class Project(models.Model):
    name = models.CharField(max_length=100)
    description = models.CharField(max_length=10000)
    creator = models.ForeignKey(ChronicleUser, null=True, on_delete=models.SET_NULL, related_name='created_projects')
    team = models.ManyToManyField(ChronicleUser, related_name='projects')
    creation = models.DateTimeField(default=datetime.now, verbose_name='Timestamp of project creation')
    image = models.ImageField(upload_to='projectImages/', default='defaults/ProjectLogo.png')
    slug = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name


class BugReport(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    reporter = models.ForeignKey(ChronicleUser, null=True, on_delete=models.SET_NULL, related_name='reported_bugs')
    heading = models.CharField(max_length=500)
    description = models.CharField(max_length=15000)
    person_in_charge = models.ForeignKey(ChronicleUser, null=True, on_delete=models.SET_NULL,
                                         related_name='bugs_assigned')
    creation = models.DateTimeField(default=datetime.now, verbose_name='Timestamp of bug report')
    status = models.BooleanField(default=False)
    tagsHash = models.IntegerField(default=0)

    class Meta:
        ordering = ['status', 'creation']

    def __str__(self):
        return f"{self.project}::{self.heading}"


class Comment(models.Model):
    report = models.ForeignKey(BugReport, on_delete=models.CASCADE)
    creation = models.DateTimeField(default=datetime.now, verbose_name='Timestamp of comment')
    commenter = models.ForeignKey(ChronicleUser, null=True, on_delete=models.SET_NULL)
    body = models.CharField(max_length=1000)

    class Meta:
        ordering = ['creation']

    def __str__(self):
        return f"{self.report.project}::{self.report}::{self.body}"


class Image(models.Model):
    randIdentifier = models.CharField(max_length=100)
    url = models.ImageField(upload_to='imageWarehouse/')
<file_sep>from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/bugReport/(?P<pk>\d+)/$', consumers.CommentConsumer)
]
<file_sep>FROM redis:5

LABEL maintainer="<NAME>"
<file_sep>from chronicles.settings import CONFIG_VARS
import json
import requests
from django.contrib.auth import login, logout
from django.http import JsonResponse, HttpResponseForbidden, HttpResponseBadRequest
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from .permissions import *
from .serializers import *
from .mailingWizard import MailThread


class UserViewSet(viewsets.ModelViewSet):
    queryset = ChronicleUser.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAdmin]

    @action(methods=['GET'], detail=False, url_path='projects', url_name='projects')
    def user_projects(self, request):
        if request.user.is_authenticated and request.user.is_active:
            serializer = ProjectSerializer(request.user.projects.all(), many=True)
            return Response(serializer.data)
        else:
            return HttpResponseForbidden()

    @action(methods=['GET'], detail=False, url_path='bugReports', url_name='bugReports')
    def user_bug_reports(self, request):
        if request.user.is_authenticated and request.user.is_active:
            serializer = BugReportVerboseSerializer(request.user.bugs_assigned.all(), many=True)
            return Response(serializer.data)
        else:
            return HttpResponseForbidden()

    @action(methods=['GET'], detail=False, url_path='curr', url_name='curr')
    def curr_user(self, request):
        if request.user.is_authenticated:
            user = request.user
            if user.is_active:
                serializer = UserSerializer(user)
                return JsonResponse(serializer.data)
            else:
                return HttpResponseForbidden()

        else:
            return HttpResponseForbidden()

    @action(methods=['GET'], detail=False, url_path='logout', url_name='logout')
    def logoutCU(self, request):
        if request.user.is_authenticated:
            logout(request)
            return JsonResponse({'status': 'Logged out'})
        else:
            return HttpResponseForbidden()

    @action(methods=['POST', 'OPTIONS'], detail=False, url_name='token', url_path='token')
    def token_parser(self, request):
        try:
            data = json.loads(request.body.decode('utf-8'))
            auth_code = data['code']
        except:
            return HttpResponseBadRequest()
        payload = {
            'client_id': CONFIG_VARS['CHANNELI']['CLIENT_ID'],
            'client_secret': CONFIG_VARS['CHANNELI']['CLIENT_SECRET'],
            'grant_type': 'authorization_code',
            'redirect_url': CONFIG_VARS['CHANNELI']['REDIRECT_URI'],
            'code': auth_code,
        }
        response = requests.post('https://internet.channeli.in/open_auth/token/', data=payload)
        resdict = json.loads(response.text)
        access_token = resdict['access_token']

        response2 = requests.get("https://internet.channeli.in/open_auth/get_user_data/",
                                 headers={'Authorization': f'Bearer {access_token}'})
        resdict2 = json.loads(response2.text)

        # Login
        roles = resdict2['person']['roles']
        maintainer = False
        for i in roles:
            if i['role'] == 'Maintainer':
                maintainer = True
        if maintainer:
            try:
                user = ChronicleUser.objects.get(enrNo=resdict2['student']['enrolmentNumber'])
                if user.is_active:
                    login(request=request, user=user)
                else:
                    return HttpResponseForbidden()
            except ChronicleUser.DoesNotExist:
                enr_no = resdict2['student']['enrolmentNumber']
                if (int(enr_no) // (10 ** 6)) % 10 == 7:
                    staff = True
                else:
                    staff = False
                user = ChronicleUser(
                    username=resdict2['person']['fullName'],
                    enrNo=resdict2['student']['enrolmentNumber'],
                    email=resdict2['contactInformation']['instituteWebmailAddress'],
                    is_staff=staff,
                    isAdmin=staff,
                    is_superuser=staff
                )
                user.save()
                login(request=request, user=user)
        return JsonResponse({'user': user.username})


class ProjectViewSet(viewsets.ModelViewSet):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    lookup_field = 'slug'
    permission_classes = [permissions.IsAuthenticated, IsProjectCreatorOrAdmin]

    def perform_create(self, serializer):
        project = serializer.save(creator=self.request.user)
        context = {
            "action": "new_team_member",
            "project": project,
        }
        MailThread(serializer.validated_data['team'], context).start()

    def perform_update(self, serializer):
        try:
            new_users = [user for user in serializer.validated_data['team'] if user not in self.get_object().team.all()]
            context = {
                "action": "new_team_member",
                "project": self.get_object(),
            }
            MailThread(new_users, context).start()
        except KeyError:
            pass
        serializer.save()


class BugReportViewSet(viewsets.ModelViewSet):
    queryset = BugReport.objects.all()
    permission_classes = [permissions.IsAuthenticated, IsTeamMemberOrAdmin]

    def perform_create(self, serializer):
        bug_report = serializer.save(reporter=self.request.user)
        context = {
            "action": "bug_report_update",
            "project": bug_report.project,
            "bug_report": bug_report,
        }
        MailThread(bug_report.project.team.all(), context).start()

    def perform_update(self, serializer):
        try:
            user = serializer.validated_data['person_in_charge']
            status = serializer.validated_data['status']

            if status:
                context = {
                    "action": "bug_resolved",
                    "project": self.get_object().project,
                    "bug_report": self.get_object(),
                }
                MailThread(self.get_object().project.team.all(), context).start()
            else:
                context = {
                    "action": "bug_report_assignment",
                    "project": self.get_object().project,
                    "bug_report": self.get_object(),
                }
                MailThread([user], context).start()
        except KeyError:
            pass
        serializer.save()

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return BugReportSerializer
        elif self.request.method == 'PATCH' or self.request.method == 'PUT':
            return BugReportEditSerializer
        else:
            return BugReportVerboseSerializer

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        if ChronicleUser.objects.get(pk=request.data['person_in_charge']) in self.get_object().project.team.all():
            return self.update(request, *args, **kwargs)
        else:
            content = {
                'status': 'Not a team member'
            }
            return Response(content)

    def update(self, request, *args, **kwargs):
        if ChronicleUser.objects.get(pk=request.data['person_in_charge']) in self.get_object().project.team.all():
            partial = kwargs.pop('partial', False)
            instance = self.get_object()
            serializer = self.get_serializer(instance, data=request.data, partial=partial)
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)
            return Response(serializer.data)
        else:
            content = {
                'status': 'Not a team member'
            }
            return Response(content)


class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    permission_classes = [permissions.IsAuthenticated, IsCommenter]

    def perform_create(self, serializer):
        serializer.save(commenter=self.request.user)

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return CommentSerializer
        else:
            return CommentEditSerializer


class MembersOfProject(APIView):

    def get(self, request, pk, format=None):
        serializer = UserSerializer(Project.objects.get(pk=pk).team.all(), many=True)
        return Response(serializer.data)


class BugsOfProject(APIView):

    def get(self, request, pk, format=None):
        serializer = BugReportSerializer(Project.objects.get(pk=pk).bugreport_set.all(), many=True)
        return Response(serializer.data)


class CommentsOnBugs(APIView):

    def get(self, request, pk, format=None):
        serializer = CommentVerboseSerializer(BugReport.objects.get(pk=pk).comment_set.all(), many=True)
        return Response(serializer.data)


class ImageViewSet(viewsets.ModelViewSet):
    queryset = Image.objects.all()
    serializer_class = ImageSerializer
    permission_classes = [permissions.IsAuthenticated]

    @action(methods=['POST'], detail=False, url_path='deleteRem', url_name='deleteRem')
    def delete_remaining_images(self, request):
        if request.user.is_authenticated:
            # try:
            randIdentifier = request.POST.get('randIdentifier')
            urls = request.POST.get('urls')

            query_set = Image.objects.filter(randIdentifier=randIdentifier)
            for i in query_set:
                if i.url.url not in urls:
                    i.delete()

            return JsonResponse({'status': 'successful'})
            # except:
            #     return HttpResponseBadRequest()
        else:
            return HttpResponseForbidden()
<file_sep>FROM nginx:alpine

LABEL maintainer="<NAME>"

RUN rm /etc/nginx/conf.d/default.conf

COPY ./configuration/ /etc/nginx/conf.d/
<file_sep>from chronicles.settings import CONFIG_VARS
import smtplib, ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import threading

import io
import yaml

with io.open('configuration/config.yml', 'r') as stream:
    CONFIG_VARS = yaml.safe_load(stream)

sender_email = CONFIG_VARS['EMAIL']['ADDRESS']
password = CONFIG_VARS['EMAIL']['PASSWORD']


def sendmail(receiver, message):
    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
        server.login(sender_email, password)
        server.sendmail(sender_email, receiver, message.as_string())


def message_generator(mail_data):
    message = MIMEMultipart("alternative")
    message["Subject"] = mail_data["subject"]
    message["From"] = sender_email
    message["To"] = mail_data["receiver_email"]
    text = mail_data["text"]
    html = mail_data["html"]
    part1 = MIMEText(text, "plain")
    part2 = MIMEText(html, "html")
    message.attach(part1)
    message.attach(part2)
    return message


def personify(user_list, context):

    frontend_url = CONFIG_VARS['FRONTEND']
    subject = ""
    text = ""
    html = ""

    if context["action"] == "new_team_member":
        subject = "Welcome Aboard!"
        text = f"""You have been added to the project {context['project'].name}"""
        html = f"""\
            <html>
                <body style='text-align:center;'>
                    <h1 style='color:purple;'>Chronicles</h1><hr />
                    <p>Congratulations on making into Project <strong>{context['project'].name}</strong>!</p><hr />
                </body>
            </html>
        """

    elif context["action"] == "bug_report_update":
        subject = f"New Bug found in {context['project'].name} 😟"
        text = f"""There is a new bug report in {context['project'].name}. 
            Visit {frontend_url+context['project'].slug}/
        """
        html = f"""\
            <html>
                <body style='text-align:center;'>
                    <h1 style='color:purple;'>Chronicles</h1><hr />
                    <p>Project <strong>{context['project'].name}</strong> has a new bug report 
                    entitled {context['bug_report'].heading}.</p>
                    <p>Visit Chronicles and fix it ASAP!</p>
                    <a href="{frontend_url+context['project'].slug}/">Link to the Project</a><hr />
                </body>
            </html>
        """

    elif context["action"] == "bug_report_assignment":
        subject = f"Bug report assigned to you!"
        text = f"""A bug of project {context['project'].name} has been assigned to you.
            Visit {frontend_url} to find out more.
        """
        html = f"""\
            <html>
                <body style='text-align:center;'>
                    <h1 style='color:purple;'>Chronicles</h1><hr />
                    <p>A bug in the project <strong>{context['project'].name}</strong> has been assigned to you.</p>
                    <p>Heading: {context['bug_report'].heading}</p>
                    <p>Visit Chronicles and fix it ASAP!</p>
                    <a style="color:'purple'; text-decoration: 'none'" href="{frontend_url}/">Chronicles</a><hr />
                </body>
            </html>
        """

    elif context["action"] == "bug_resolved":
        subject = f"Bug report resolved 😊"
        text = f"""A bug of project {context['project'].name} has been resolved!"""
        html = f"""\
            <html>
                <body style='text-align:center;'>
                    <h1 style='color:purple;'>Chronicles</h1><hr />
                    <h3>Wohoo!</h3>
                    <p>A bug report from project <strong>{context['project'].name}</strong> was resolved!</p>
                    <h4>Heading of bug report:</h4>
                    <p>{context['bug_report'].heading}</p><hr />
                </body>
            </html>
        """

    if (subject != "") and (text != "") and (html != ""):
        for user in user_list:
            mail_data = {
                "receiver_email": user.email,
                "subject": subject,
                "text": text,
                "html": html,
            }

            message = message_generator(mail_data)
            print("Sending email")
            sendmail(user.email, message)
            print("Mail sent")
    else:
        print("No data")


class MailThread(threading.Thread):
    def __init__(self, user_list, context, *args, **kwargs):
        self.user_list = user_list
        self.context = context
        super(MailThread, self).__init__(*args, **kwargs)

    def run(self):
        personify(self.user_list, self.context)
        return
<file_sep>from django.urls import path, include
from rest_framework.routers import DefaultRouter
from chronicles_backend.views import *
from django.conf.urls import include

router = DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'projects', ProjectViewSet)
router.register(r'bugReports', BugReportViewSet)
router.register(r'comments', CommentViewSet)
router.register(r'images', ImageViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('projects/<int:pk>/team/', MembersOfProject.as_view()),
    path('projects/<int:pk>/bugReports/', BugsOfProject.as_view()),
    path('bugReports/<int:pk>/comments/', CommentsOnBugs.as_view()),
]

<file_sep>from django.contrib import admin
from .models import *


admin.site.register(ChronicleUser)
admin.site.register(Project)
admin.site.register(BugReport)
admin.site.register(Comment)
admin.site.register(Image)
<file_sep>from rest_framework import permissions


# Permissions for Project View
class IsProjectCreatorOrAdmin(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if (request.method in permissions.SAFE_METHODS) or (request.method == 'POST'):
            return True
        return (obj.creator == request.user) or request.user.isAdmin


class IsTeamMemberOrAdmin(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if (request.method == 'POST') or (request.method in permissions.SAFE_METHODS):
            return True
        if request.user.isAdmin or (request.user in obj.project.team.all()):
            return True
        return False


class IsCommenter(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if (request.method in permissions.SAFE_METHODS) or (request.method == 'POST'):
            return True
        return request.user == obj.commenter


class IsAdmin(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if (request.method in permissions.SAFE_METHODS) or request.user.isAdmin:
            return True
        return False
<file_sep># Chronicles
This repository houses code for **Chronicles**, a platform where debuggers and developers unite to exterminate bugs. You are looking at the backend of the project with all the files required for dockerising it's implementation. Do visit the [Frontend](https://github.com/aitalshashank2/Chronicles-Frontend) of this project.

## Features
- Create Multiple Projects with a unique team.
- Secure Login and Registration using OAuth2.0.
- Report bugs in a project and use various in-built tags to highlight them.
- Implementation of RichText Fields for descriptions of Project and Bugs.
- Option to upload images in the description for Bug Reports.
- Real-time commenting on Bug Reports using web sockets
- Option to assign a bug report to a team member.
- Show the status of Bug Report
- Look at your Projects and the Bugs Reports assigned to you, all in one place

## Set up instructions

- Clone this repository and change directory
  ```bash
  git clone https://github.com/aitalshashank2/Chronicles.git
  cd Chronicles
  ```

- Clone the Frontend Web Application inside the `frontend/` directory
  ```bash
  cd frontend/
  git clone https://github.com/aitalshashank2/Chronicles-Frontend.git
  ```

- Copy `code/configuration/config-stencil.yml` to `code/configuration/config.yml` and populate the values.
  ```bash
  cd ../code/configuration/
  cp config-stencil.yml config.yml
  ```
  
- Build the project after returning to the base directory (containing the `docker-compose.yml` file)
  ```bash
  docker-compose build
  ```

- Run the project
  ```bash
  docker-compose up -d
  ```

- Visit **Chronicles** on [http://localhost:54330/](http://localhost:54330/)

- In order to stop the project, navigate to the base directory (containing the `docker-compose.yml` file) and run the following command
  ```bash
  docker-compose down
  ```

### Happy Coding!
<file_sep>FROM node:14.4.0-alpine3.10

LABEL maintainer="<NAME>"

USER root

RUN mkdir /frontend

WORKDIR /frontend

COPY ./Chronicles-Frontend/ .

RUN npm install

RUN npm run build
