{"id":15589808,"url":"https://github.com/spapas/feature-requests","last_synced_at":"2025-03-04T09:40:43.092Z","repository":{"id":66174508,"uuid":"156698786","full_name":"spapas/feature-requests","owner":"spapas","description":"A simple application in Flask","archived":false,"fork":false,"pushed_at":"2018-11-10T22:28:00.000Z","size":119,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-14T13:35:05.733Z","etag":null,"topics":["flask","orm","python","sql-alchemy","tutorial"],"latest_commit_sha":null,"homepage":"https://spapas.github.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spapas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-08T11:47:03.000Z","updated_at":"2018-11-10T22:28:01.000Z","dependencies_parsed_at":"2023-02-20T17:15:47.363Z","dependency_job_id":null,"html_url":"https://github.com/spapas/feature-requests","commit_stats":{"total_commits":74,"total_committers":1,"mean_commits":74.0,"dds":0.0,"last_synced_commit":"ef3203fed0c7575d32bdfd402379a734d4f28a47"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spapas%2Ffeature-requests","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spapas%2Ffeature-requests/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spapas%2Ffeature-requests/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spapas%2Ffeature-requests/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spapas","download_url":"https://codeload.github.com/spapas/feature-requests/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241827081,"owners_count":20026599,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flask","orm","python","sql-alchemy","tutorial"],"created_at":"2024-10-02T23:10:09.098Z","updated_at":"2025-03-04T09:40:43.062Z","avatar_url":"https://github.com/spapas.png","language":"Python","readme":"# Feature requests application\n\nA simple application in Flask\n\n## Tools used and rationale\n\nI wanted to keep this project as simple as possible (so as to follow closely the KISS principle) so most\nchoises will be because of that:\n\n* Python 3\n* Flask as the main web framework\n* SqlAlchemy as an ORM for database operations\n* Mysql (actually MariaDB) as the main database\n* Flask-Migrate for db migration handling\n* WTForms for form validation\n* Jinja2 for templates\n* gunicorn for serving\n* Cleave.js for date \"cleaving\"\n* Spectre minimalistic css framework\n* Fabric to deploy the application\n* Various libs for properly integrating the above\n\nPython 3 is (finally) here to stay and I use it in all my new projects!\nI used Flask because I had some experience with it (I've even written a Flask-Mongodb-Heroku-API tutorial back in 2014:\nhttps://spapas.github.io/2014/06/30/rest-flask-mongodb-heroku/) and I know that with a little (or maybe a lot of) work\nit can have most of Django's capabilities. I chose MySQL/MariaDB mainly because it feels easy and I wanted a change from\nPostgreSQL which I usually use in production projects. For deploying the python application (wsgi) I chose gunicorn due\nto its simplicitly especially when compared with other solutions (I'm looking at you, uwsgi). I had used spectre in a\ncouple of hobby projects before (ie https://github.com/spapas/hyperapp-tutorial) and seemed good enough for this application.\nFinally, Fabric is my go-to tool for automatic deployment of changes to prod/uat.\n\n## Project description\n\nThis is a simple Flask application with a small number of normal HTTP views.\nIn my opinion there's no need for REST/ajax or anything fancy; the application\ncan be implemented and have an excellent UX with good-old HTTP request/response views\nand almost no javascript. The only\nJS enhancements is the usage of cleave-js to help formatting the dates. I prefer\nit much more than a traditional approach (like f.e jquery-ui datepicker) because it \nseems much more intuitive and a quick and dirty trick that I've used to display\nthe description of a feature request in a modal popup (more on this later). \nBeyond these, everything else is pure HTML.\n\nThe application uses three main models:\n\n* `FeatureRequest`: This is the main model that the application is about. It\n  contains the fields `id` (primary key), `name`, `description` (text field), `client`\n  (foreign key to Client model),\n  `client_priority` (integer field, will be unique for each client's feature\n  requests), `target_date` (the desired date for this feature request) and \n  `product_area` (foreign key to the `ProductArea` model)\n* `Client`: This is a simple model with a name that will be used to persist the\n  clients that require the features\n* `ProductArea`: Another simple model similar to Client, will persist the product\n  areas of each feature request.\n\nI've added the following Feature Request views:\n\n* A list view (`/feature_requests/view/`): This is the main page of the application; the user can see all\n  the existing feature requests along with most of their info (actually all the\n  info is there except the feature request description which is a bit too long\n  for that). The description can be seen in a modal if you click on the \n  name of each feature requests. For displaying it in the\n  modal I use a nice but Q+D trick (encode the description to JS compatible text to\n  avoid XSS attacks and just dump it to the modal content using vanilla JS; it\n  is very simple but gets the job done).\n  The feature requests are displayed in a table (with a missing\n  pagination and sort funtionality) but at least there's proper filtering by\n  various fields of the feature requests. There's even an `overdue` checkbox\n  to filter by the feature requests that a past target date.\n* A create view (`/feature_requests/create/`): It displays a form for inserting a new feature request object\n  to the database. The client and product area fields are selects that take their\n  values from the corresponding mode values. The target date uses cleave.js to\n  properly format the date. There's proper validation for all fields (i.e the\n  date must have correct format, the values cannot be empty, the selects must\n  have values from the corresponding objects etc). When the form is submitted\n  it will redirect to the list view with a success flash message displayed. If\n  the inserted feature request has the same priority for the same client then\n  all the other feature requests for the same client that have a priority \n  equal or more than the inserted one's priority have their priority increased\n  by one. This is easier to understand by taking a look at the corresponding\n  python SQLAlchemy statement:\n\n```\n\n        if db.session.query(\n            FeatureRequest.query.filter(\n                FeatureRequest.client_priority == form.data[\"client_priority\"],\n                FeatureRequest.client_id == form.data[\"client\"].id,\n            ).exists()\n        ).scalar():\n            FeatureRequest.query.filter(\n                FeatureRequest.client_priority \u003e= form.data[\"client_priority\"],\n                FeatureRequest.client_id == form.data[\"client\"].id,\n            ).update({\"client_priority\": FeatureRequest.client_priority + 1})\n```\n\n* An update view (`/feature_requests/update/id`): This is more or less similar to the create view; it just\n  updates the object with the same validations as those described above. It\n  also has the same client priority for the same client check and fix as the\n  create view.\n\n* A delete view (`/feature_requests/delete/id`): A simple view to delete feature requests. It only works with\n  http POSTS and is called from the list view.\n\n## Project structure\n\nThis is a rather simple project. It has just one package (`core`) that\ncontains everything:\n\n* The `__init__.py` file contains the Flask app and database initialization.\n* The `forms.py` has the definition of the FeatureRequest form\n* The `models.py` contains the ORM definition of the database tables\n* The `util.py` contains a simple utility function\n* The `views.py` contains the definition of the various views that are used\n\nThere's also a template directory with the jinja2 templates and a static\ndirectory with a buch of css and js files. In the main directory I have\na `fabfile.py` to be used by Fabric to auto-deploy the app, an `init_data.py`\nthat can be run to fill the `Client` and `ProductArea` models with some\ninitial values and the `test_core.py` which tests all views of `core` \n(and actually gathers the tests of the application).\n\n# Further enhancements\n\nThere are a lot of things that are missing from this application. I've only\nimplemented the basic requirements. Most of the missing things\nare rather simple to be implemented but they will need implementation time\nwhich I lack right now.\nHowever as a bonus I'll present a list of the things I'd like to have to\nconsider this application as (mostly) complete along with a small description on how these could be implemented.\n\n* Users: A user component is definitely missing. The feature requests should\n  save the user that added them and also it would be nice if each feature\n  request could be assigned to a user for implementing it. To support users\n  a package like Flask-user (https://flask-user.readthedocs.io/en/latest/)\n  would be used; I'd then add two ForeignKeys to the FeatureRequest model one\n  with the user that created the FeatureRequest and one (nullable) with the\n  user that has been assigned this `FeatureRequest`. The first foreign key would\n  be auto-filled when the object would be created, the 2nd could just go to\n  FeatureRequest form as a select input containing all users.\n* Display the description of each FeatureRequest. The description is a Text\n  field (i.e it can be rather long) so I didn't put it in the table. Right\n  now you can see it through a modal if you click on the feature request's\n  name or if you edit the table; this isn't ideal from a UX point of view. \n  For me the best way would be the classic Djangoish one, ie to just add\n  a Detail view for that FeatureRequest where you'll get a full page with all\n  the information of that FeatureRequest along with proper action buttons \n  (update, delete) in the bottom of that detail view.\n* Pagination: This is definitely needed. Flask-SqlAlchemy supports it out of\n  the box so I'd just need to pass the correct `?page=x` query parameter and\n  then limit the results by returning the objects from `page * page_number` to\n  `(page+1) * page_number`.\n* Table fields ordering: This is a nice to have if you have a table: Click on\n  the \u003cth\u003e of a field and sort by this field ascending. Click again and sort\n  descending by the same field. This is also easy (but needs work) to implement:\n  Just add an `?ordering=field_name` parameter to the request. If the `field_name`\n  starts with a `-` sort descending by that field else sort ascending. Notice\n  that needs a lot of work in the template also since each \u003cth\u003e of the table \n  needs to properly add the correct `?ordering=` to the request parameters;\n  and also switch between the ascending and descending sorting.\n* Delete confirmation: This is needed to avoid accidentally deleting objects.\n  The easiest way would be to use Javascript ``confirm()`` (i.e https://stackoverflow.com/questions/9334636/how-to-create-a-dialog-with-yes-and-no-options)\n  however it is so ugly that I never want to use it. So probably a component\n  like Sweet-alert (https://sweetalert2.github.io/) would make everything \n  more beautiful. The other way to do this is to create a GET response for\n  the delete view and redirect to that view when the user clicks the delete\n  button. That delete view would only contain a yes/no form and when the user\n  clicks yes it will do the HTTP POST to the delete view in order to actually\n  remove the object.\n* Mark finished Featured Requests. An `is_finished` boolean field is needed\n  to mark the feature requests that have been implemented; we don't want to\n  get stressed that we have overdue feature requests when we've actually finished\n  them! Probably also add a `POST` mark as finished view to be able to mark the \n  feature request as finished without the need to actually display the upgrade\n  form. Actually, each `FeatureRequest` could have more state instead of a\n  boolean one (finished or not), for example something like `FUTURE`,\n  `SPECS`, `DEVELOPING`, `TESTING`, `COMPLETE` etc along with the proper\n  `POST` view to change its state.\n* Stats/aggregates: I really like stats so I'd definitely add some\n  stats like how many feature requests per client / per priority / per \n  target area / that are overdue etc. Also it would be nice to tell each client\n  that the previous year/month we implemented N of your `FeatureRequests` and\n  each one of them in so much time and with none of them overdue! Of course to\n  have proper stats we'd need to keep more info about each `FeatureRequest`\n  like when it was reported first, when it was completed, if it was overdue etc.\n* Autocompletes: Well if you have many clients (or many product areas or\n  many users if you have implemented my first suggestion) you'll probably need\n  a proper autocomplete for that field. Just add a simple view that would get\n  the `?term=` as a request parameter and query the `Client` model by names\n  starting with the term. It should probably return the data in a JSON array;\n  yes some people would argue that since we want to keep it simple why not\n  return strings separated with commas - the problem with that is that the\n  JSON encoding is a solved problem, the \"string separated with commas\" encoding\n  is a problem waiting to bite you in the foot when you have clients that have\n  commas in their names. In any case, for the actual autocomplete widget I\n  have great experience with select2 (https://select2.org/) thus that's what\n  I'd use.\n* `Client` and `ProductArea` CRUDs: This is definitely needed since Flask doesn't\n  have a Django Admin! Implementing all the views and templates for these\n  two models is really simple but needs hard work; I won't even go to the\n  detail of how to implement this (just do the same that I did with \n  FeatureRequest but with a simple form). For now either add values to the\n  these models through the database or just run `init_data.py` to auto\n  add some values to these models.\n* Full Auditing: I like to know which user changed which fields of each `FeatureRequest` or \n  at least which user added and last edited each `FeatureRequest`. Here's a post for\n  model auditing in Django to give you some ideas: https://spapas.github.io/2015/01/21/django-model-auditing/\n\n## Flask vs Django\n\nPlease notice that I'm a rather experience Django developer (check out my \nblog for some more Django articles if you want https://spapas.github.io/category/django.html)\nthat's why I am addng this section (and also that's why some of my\ncomments here refer to Django).\n\nFor this reason, one thing that I feel obligated to add here is that if I'd used Django\ninstead of Flask most of the above would be trivial and really quick to be\nimplemented (just change some settings or use a django-package and change some\nsettings). For example, Users are built-in in Django, table operations \n(pagination, ordering, pretty tables etc) are\noffered through django-tables2, django-filters is great for filtering,\ndjango-autocomplete-light has excellent select2 support, django has Detail\nand DeleteView. Check out my essential django packages list for more\nideas: https://spapas.github.io/2017/10/11/essential-django-packages/\n\nFlask seems like a really nice framework however you must be very careful in\nwhich applications you are going to use it. It probably is good for something\nsimple that uses a couple of REST views but if you want to implement more\ncomplex things then you are going to slowly and painfully research, gather and integrate\nvarious things that Django (and its packages) offers you in the plate. So why not just eat from\nthat  plate?\n\n## How to develop\n\nCreate a virtualenv and install the requirements. Then you should add a file\nnamed `local.py` in the `instance` folder by copying the `local.py.template`\nand adding the proper settings. If you want to use Sqlite3 for development\nadd something like\n\n``` python\nimport os\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\nSQLALCHEMY_DATABASE_URI='sqlite:///' + os.path.join(basedir, 'data.sqlite')\n```\n\nIf you want to use mysql: \n\n``` python\nSQLALCHEMY_DATABASE_URI='mysql+pymysql://user:pass@host/database'\n```\n\nThen you should set the `FLASK_APP` environment to `core` (i.e `SET FLASK_APP=core`\nin Windows cmd or `export FLASK_APP=core` in bash), create the migrations by running `flask db upgrade` and\nload some initial data by running `python init_data.py`. Finally you can\nrun the server by running `flask run`.\n\n\n## Testing\n\nThe file `test_core.py` contains proper tests for all the views of the application. I didn't think that any\nmore tests would be required for such a simple application. The part of the code that would need the most\ntesting would be the functionality of changing the client priorities when there's a conflict. This could \nhave been moved to a separate module (i.e `services.py` or something) so that it'd be called from the views\nand the testing code would be to explicitly call that code. However because of the way it's been implemented\n(i.e it will need to read and update the database) and due to the size of the app (very small and simple) I\ndidn't feel that putting it in a separate module would offer much thus I left it inside the view; since the\ntests that check the view properly check that the conflicting client priorities have been updated correctly\nthat part should be considered properly tested.\n\n## How to deploy\n\nI've deployed the app in an Ubuntu 18.04 AWS EC2 instance. I installed Mariadb\n10.1 from the repositories. I then created a folder in `/home/ubuntu` named\n`feature-requests` in which I created a python 3 virtual environment an then\ncloned the https://github.com/spapas/feature-requests github repo. I then\nconfigured the instance/local.py similar to dev to connect to the local\nMariadb instance. \n\nFor serving the application I used gunicorn. To properly start and stop\ngunicorn I created a systemctl service for the application, you can find\nit at `feature-requests/etc/gunicorn.service`. This file should be copied to\n`/etc/systemd/system/gunicorn.service` and then run:\n\n```\nsudo systemctl start gunicorn\nsudo systemctl enable gunicorn\n```\n\nMore info on this great tutorial here: \nhttps://www.digitalocean.com/community/tutorials/how-to-serve-flask-applications-with-gunicorn-and-nginx-on-ubuntu-18-04\n\nI also used nginx (installed from the repositories) to serve the static files\nand as a reverse proxy for the application (i.e nginx is listening to port 80\nand forwards requests to the app to gunicorn, \ngunicorn is listening to port 8000 and communicates through nginx). I just\nchanged the nginx default configuration (found in `/etc/nginx/sites-available/default`) \nwith the one found in `feature-requests/etc/nginx-default` and you should be good to go!\n\nFinally don't forget to load the migrations by running `FLASK_APP=core flask db upgrade`\nand load the initial data `FLASK_APP python init_data.py` (from the app home directory).\n\n## Fully scripted deploy with Fabric\n\nI am using Fabric 1.x in all my projects to quickly deploy changes to production\n(or uat) and I am really happy with it, for example check out the `fabfile.py` for this\nproject: https://github.com/spapas/mailer_server\n\nFor the Feature Requests app I decided to try my luck in upgrading my fabfile to use Fabric 2.x so\nthat everything would be Python 3.x; in my previous Python 3.x projects I was using my good-old Fabric 1.x fabfile\nso I was using a Python 2.7 Fabric 1.x to actually run the fabfile. \n\nWell, I regretted that decision! Fabric 2.x has way too many changes and there's too little documentation\n(and proper SO answers) for using it. Thus I had to fall back to implementing a rather simple fabfile that\nwas able to get the job done nevertheless: This fabfile has four tasks: \n\n* pull: To retrieve the latest changesets  from the remote github repo\n* work: To install any new requirements and run migration upgrades\n* restart: To restart the gunicorn application using systemctl\n* full-deploy: To run all the above\n\nTo configure it just set `hosts` = `[ user@hostname ]` in that fabfile using the user and hostname where you want to deploy. To\nrun it try something `fab -i /path/to/amazon-aws-key.pem full-deploy`.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspapas%2Ffeature-requests","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspapas%2Ffeature-requests","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspapas%2Ffeature-requests/lists"}