फ्लास्क मेगा-ट्यूटोरियल, भाग 3: फॉर्म

यह एक श्रृंखला का तीसरा लेख है जहां मैं फ्लास्क माइक्रोफ्रामवर्क का उपयोग करके पायथन वेब एप्लिकेशन लिखने के अपने अनुभव का वर्णन करता हूं।

इस गाइड का उद्देश्य एक काफी कार्यात्मक माइक्रोब्लॉगिंग एप्लिकेशन विकसित करना है, जिसे मैंने माइक्रोब्लॉग को मौलिकता की पूर्ण कमी के लिए कॉल करने का निर्णय लिया।



संक्षिप्त पुनरावृत्ति


पिछले भाग में, हमने होम पेज के लिए एक सरल टेम्पलेट को परिभाषित किया और काल्पनिक वस्तुओं का उपयोग उन चीजों के प्रोटोटाइप के रूप में किया, जिन्हें हमने अभी तक नहीं किया है। उदाहरण के लिए, उपयोगकर्ता या रिकॉर्ड।

इस लेख में, हम एक अंतराल को भरने जा रहे हैं जो हमारे आवेदन में मौजूद है। हम रूपों के साथ काम करने पर विचार करेंगे।

फॉर्म किसी भी वेब एप्लिकेशन में सबसे बुनियादी ब्लॉकों में से एक हैं। प्रपत्रों के उपयोग से उपयोगकर्ता ब्लॉग प्रविष्टियों को छोड़ सकते हैं, साथ ही आवेदन में लॉग इन कर सकते हैं।

इस भाग का पालन करने के लिए, आपका माइक्रोब्लॉगिंग एप्लिकेशन वैसा ही होना चाहिए जैसा कि हमने पिछले एक के अंत में छोड़ा था। कृपया सुनिश्चित करें कि एप्लिकेशन इंस्टॉल और काम कर रहा है।


विन्यास


प्रसंस्करण रूपों के लिए, हम Flask-WTF एक्सटेंशन का उपयोग करेंगे, जो एक Flask-WTF WTForms आवरण है और फ्लास्क अनुप्रयोगों के साथ पूरी तरह से एकीकृत होता है।

कई फ्लास्क एक्सटेंशन को कुछ कॉन्फ़िगरेशन की आवश्यकता होती है, इसलिए हम अपने माइक्रोब्लॉग रूट फ़ोल्डर के अंदर एक कॉन्फ़िगरेशन फ़ाइल बनाएंगे, ताकि यदि आवश्यक हो तो यह आसानी से मोड्यूल के लिए सुलभ हो जाएगा। यहाँ हम शुरू (config.py फ़ाइल):
 CSRF_ENABLED = True SECRET_KEY = 'you-will-never-guess' 


यह सरल है, ये दो सेटिंग्स हैं जिन्हें हमारे Flask-WTF एक्सटेंशन की आवश्यकता है। CSRF_ENABLED नकली क्रॉस-साइट अनुरोधों की रोकथाम को सक्रिय करता है। ज्यादातर मामलों में, आप इस विकल्प को सक्षम करना चाहेंगे, जिससे आपका आवेदन अधिक सुरक्षित हो जाएगा।

SECRET_KEY आवश्यकता तभी है जब CSRF सक्षम हो। इसका उपयोग क्रिप्टोग्राफिक टोकन बनाने के लिए किया जाता है जो कि फॉर्म सत्यापन में उपयोग किया जाता है। जब आप अपना आवेदन लिखते हैं, तो सुनिश्चित करें कि आपकी गुप्त कुंजी को चुनना मुश्किल है।

अब हमारे पास कॉन्फ़िगरेशन है, और हमें फ्लास्क को इसे पढ़ने और उपयोग करने के लिए कहना चाहिए। फ्लास्क एप्लिकेशन ऑब्जेक्ट बनने के बाद हम यह अधिकार कर सकते हैं। (फ़ाइल ऐप / __ init__.py):
 from flask import Flask app = Flask(__name__) app.config.from_object('config') from app import views 


लॉग इन फॉर्म


Flask-WTF रूपों को Form क्लास से उपवर्ग वस्तुओं के रूप में दर्शाया जाता है। एक प्रपत्र उपवर्ग क्लास में चर के रूप में फॉर्म फ़ील्ड को परिभाषित करता है।

हम एक लॉगिन फ़ॉर्म बनाएंगे जिसका उपयोग पहचान प्रणाली के साथ किया जाएगा। हम अपने एप्लिकेशन में जिस लॉगिन तंत्र का समर्थन करेंगे, वह मानक प्रकार का उपयोगकर्ता नाम / पासवर्ड नहीं है - हम OpenID का उपयोग लॉगिन के रूप में करेंगे। OpenID का लाभ यह है कि प्राधिकरण OpenID प्रदाता के माध्यम से पारित किया जाता है, इसलिए हमें पासवर्ड की जांच करने की आवश्यकता नहीं है, जिससे हमारी साइट हमारे उपयोगकर्ताओं के लिए अधिक सुरक्षित हो जाएगी।

OpenID लॉगिन के लिए केवल एक पंक्ति की आवश्यकता होती है जिसे OpenID कहा जाता है। हम फॉर्म में याद रखें मुझे चेकबॉक्स को भी छोड़ देंगे ताकि उपयोगकर्ता अपने ब्राउज़र में एक कुकी सेट कर सके, जो वापस लौटने पर उनके उपयोगकर्ता नाम को याद रखेगा।

हम अपना पहला फॉर्म लिखेंगे (फ़ाइल ऐप / फॉरमसडीएम):
 from flask.ext.wtf import Form from wtforms import TextField, BooleanField from wtforms.validators import Required class LoginForm(Form): openid = TextField('openid', validators = [Required()]) remember_me = BooleanField('remember_me', default = False) 


मुझे यकीन है कि वर्ग खुद के लिए बोलता है। हमने Form क्लास और दो फील्ड क्लास की जरूरत है, BooleanField TextField और BooleanField

एक आयातित Required एक सत्यापनकर्ता है, एक फ़ंक्शन जिसे उपयोगकर्ता द्वारा भेजे गए डेटा को मान्य करने के लिए एक फ़ील्ड से जोड़ा जा सकता है। Required सत्यापनकर्ता केवल यह सत्यापित करता है कि फ़ील्ड रिक्त नहीं भेजा गया था। Flask-WTF में कई सत्यापनकर्ता हैं, हम भविष्य में कई नए उपयोग करेंगे।

प्रपत्र टेम्पलेट


हमें एक HTML टेम्पलेट की भी आवश्यकता है जिसमें एक फॉर्म होता है। अच्छी खबर यह है कि हमने जो LoginForm क्लास बनाई है, वह जानती है कि HTML में फॉर्म फील्ड कैसे रेंडर किए जाते हैं, इसलिए हमें सिर्फ लेआउट पर ध्यान देने की जरूरत है। यहां हमारा लॉगिन टेम्प्लेट है: (एप्लिकेशन / टेम्प्लेट / लॉगिन.html फ़ाइल):
 <!-- extend from base layout --> {% extends "base.html" %} {% block content %} <h1>Sign In</h1> <form action="" method="post" name="login"> {{form.hidden_tag()}} <p> Please enter your OpenID:<br> {{form.openid(size=80)}}<br> </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form> {% endblock %} 


ध्यान दें कि हम फिर से इनहेरिटेंस ऑपरेटर के माध्यम से base.html टेम्पलेट का उपयोग कर रहे हैं, इसे बढ़ा रहे हैं। हम अपने सभी टेम्प्लेट के साथ ऐसा करेंगे, यह सुनिश्चित करते हुए कि लेआउट सभी पृष्ठों के अनुरूप है।

नियमित HTML फॉर्म और हमारे टेम्पलेट के बीच कुछ दिलचस्प अंतर हैं। टेम्प्लेट से हमें फ़ार्म क्लास के एक उदाहरण की उम्मीद होती है जिसे हमने form टेम्प्लेट तर्क में निर्दिष्ट किया है। हम भविष्य में इस टेम्प्लेट तर्क को भेजने का ध्यान रखेंगे जब हम इस टेम्प्लेट को वापस करने वाले दृश्य फ़ंक्शन को लिखते हैं।

हमारी सेटिंग फ़ाइल में शामिल CSRF को रोकने के लिए टेम्पलेट पैरामीटर form.hidden_tag() को एक छिपे हुए फ़ील्ड के साथ बदल दिया जाएगा। यदि CSRF सक्षम है तो यह फ़ील्ड आपके सभी रूपों में होनी चाहिए।

हमारे फॉर्म के फ़ील्ड फॉर्म ऑब्जेक्ट द्वारा दिए गए हैं, आपको बस उस टेम्पलेट के स्थान पर तर्क {{form.field_name}} को संदर्भित करना होगा जहां फ़ील्ड डाला जाना चाहिए। कुछ क्षेत्र तर्क ले सकते हैं। हमारे मामले में, हम फॉर्म को 80 अक्षरों की चौड़ाई के साथ हमारे खुले मैदान बनाने के लिए कहते हैं।

चूंकि हमने फॉर्म क्लास में सबमिट बटन को परिभाषित नहीं किया है, इसलिए हमें इसे एक नियमित क्षेत्र के रूप में परिभाषित करना चाहिए। सबमिट फ़ील्ड कोई डेटा नहीं लेती है, इसलिए इसे फॉर्म क्लास में परिभाषित करने की कोई आवश्यकता नहीं है।

फॉर्म सबमिशन


अंतिम चरण से पहले हम अपने फॉर्म को देख सकते हैं एक प्रस्तुति फ़ंक्शन लिखना है जो टेम्पलेट प्रदान करता है।

यह वास्तव में काफी सरल है, क्योंकि हमें केवल टेम्पलेट के लिए फॉर्म ऑब्जेक्ट को पास करने की आवश्यकता है। यहाँ हमारा नया व्यू फंक्शन है (ऐप / व्यूहोम फ़ाइल):
 from flask import render_template, flash, redirect from app import app from forms import LoginForm #   index    @app.route('/login', methods = ['GET', 'POST']) def login(): form = LoginForm() return render_template('login.html', title = 'Sign In', form = form) 


हमने अपनी LoginForm कक्षा का आयात किया, इसका एक उदाहरण बनाया और इसे खाके पर भेजा। यह आपको फॉर्म फ़ील्ड बनाने की आवश्यकता है।

हम flash और redirect आयात करने पर ध्यान नहीं देंगे। हम थोड़ी देर बाद उनका उपयोग करेंगे।

एक अन्य नवाचार route सज्जाकार में विधि तर्क है। यहाँ हम फ्लास्क को बताते हैं कि व्यू फंक्शन GET और POST अनुरोध को स्वीकार करता है। इसके बिना, सबमिशन केवल GET अनुरोध स्वीकार करेगा। हम POST अनुरोध प्राप्त करना चाहते हैं जो उपयोगकर्ता द्वारा दर्ज किए गए डेटा के साथ एक फॉर्म सबमिट करेगा।

इस स्तर पर, आप एप्लिकेशन लॉन्च कर सकते हैं और ब्राउज़र में अपने फ़ॉर्म को देख सकते हैं। शुरू करने के बाद, हम उस फ़ंक्शन को खोलें जिसे हमने फ़ंक्शन लॉग इन के साथ संबद्ध किया है: http: // localhost: 5000 / लॉगिन

हमने डेटा प्राप्त करने वाले भाग को प्रोग्राम नहीं किया है, इसलिए सबमिट बटन पर क्लिक करने से कोई प्रभाव नहीं पड़ेगा।

प्रपत्र डेटा पुनर्प्राप्त कर रहा है
एक अन्य क्षेत्र जहां Flask-WTF हमारे काम को आसान बनाता है, भेजे गए डेटा की प्रोसेसिंग है। यह हमारे login दृश्य फ़ंक्शन का एक नया संस्करण है, जो फ़ॉर्म डेटा (ऐप / व्यूज़ो फ़ाइल) को मान्य और बचाता है:
 @app.route('/login', methods = ['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data)) return redirect('/index') return render_template('login.html', title = 'Sign In', form = form) 


validate_on_submit विधि संपूर्ण प्रसंस्करण करती है। यदि आप विधि को उस रूप में बुलाते हैं, जब उपयोगकर्ता को फ़ॉर्म प्रस्तुत किया जाता है (यानी उपयोगकर्ता को वहाँ डेटा दर्ज करने का अवसर मिलता है), तो वह False लौटा देगा, इस स्थिति में आप जानते हैं कि आपको खाका खींचना होगा।

यदि validate_on_submit को फॉर्म validate_on_submit रिक्वेस्ट के हिस्से के रूप में एक साथ कहा जाता है, तो यह सभी डेटा एकत्र करेगा, खेतों से जुड़े किसी भी सत्यापनकर्ता को चलाएगा, और यदि सब कुछ ठीक है, तो यह True लौटेगा, जो डेटा की वैधता को इंगित करता है। इसका मतलब है कि डेटा एप्लिकेशन में शामिल करने के लिए सुरक्षित है।

यदि कम से कम एक फ़ील्ड सत्यापन पास नहीं करता है, तो फ़ंक्शन False वापस आ जाएगा और इससे उपयोगकर्ता के सामने फ़ॉर्म फिर से आ जाएगा, जिससे त्रुटियों को सही करना संभव होगा। बाद में हम सीखेंगे कि सत्यापन विफल होने पर त्रुटि संदेश कैसे प्रदर्शित करें।

जब validate_on_submit True रिटर्न करता है, तो हमारा दृश्य फ़ंक्शन फ्लास्क से आयात किए गए दो नए फ़ंक्शन को कॉल करता है। Flash फ़ंक्शन उपयोगकर्ता को प्रस्तुत अगले पृष्ठ पर एक संदेश प्रदर्शित करने का एक त्वरित तरीका है। इस स्थिति में, हम डिबगिंग के लिए इसका उपयोग करेंगे जब तक कि हमारे पास लॉगिंग के लिए आवश्यक बुनियादी ढांचा है, इसके बजाय हम बस एक संदेश प्रदर्शित करेंगे जो भेजे गए डेटा को दिखाएगा। उपयोगकर्ता प्रतिक्रिया प्रदान करने के लिए उत्पादन सर्वर पर flash भी बेहद उपयोगी है।

फ़्लैश संदेश स्वचालित रूप से हमारे पेज पर दिखाई नहीं देंगे, हमारे टेम्प्लेट्स को संदेशों को उस रूप में प्रदर्शित करना चाहिए जो हमारी साइट के लेआउट के अनुरूप है। हम बेस टेम्प्लेट में संदेश जोड़ देंगे, ताकि हमारे सभी टेम्पलेट इस कार्यक्षमता को प्राप्त कर लें। यह एक अपडेटेड बेस टेम्प्लेट (फाइल एप / टेम्प्लेट / बेस। Html) है:
 <html> <head> {% if title %} <title>{{title}} - microblog</title> {% else %} <title>microblog</title> {% endif %} </head> <body> <div>Microblog: <a href="/index">Home</a></div> <hr> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }} </li> {% endfor %} </ul> {% endif %} {% endwith %} {% block content %}{% endblock %} </body> </html> 


मुझे उम्मीद है कि जिस तरह से संदेश प्रदर्शित किया गया है उसे स्पष्टीकरण की आवश्यकता नहीं है।

एक और नई सुविधा जो हमने अपने लॉगिन दृश्य में उपयोग की है वह redirect । यह फ़ंक्शन अनुरोध किए गए एक के बजाय क्लाइंट वेब ब्राउज़र को दूसरे पेज पर रीडायरेक्ट करता है। हमारे प्रस्तुति समारोह में, हमने पिछले भागों में विकसित मुख्य पृष्ठ पर पुनर्निर्देशित किया। ध्यान रखें कि यदि फ़ंक्शन रीडायरेक्ट के साथ समाप्त होता है तो भी फ्लैश संदेश प्रदर्शित किए जाएंगे।

आवेदन को चलाने और फॉर्म कैसे काम करते हैं, इसकी जांच करने के लिए बहुत अच्छा समय है। यह देखने के लिए कि खाली सत्यापनकर्ता प्रस्तुत प्रक्रिया को कैसे रोकता है, खाली ओपेन फ़ील्ड के साथ एक फॉर्म सबमिट करने का प्रयास करें।

फील्ड सत्यापन में सुधार


इसकी वर्तमान स्थिति में आवेदन के साथ, अमान्य डेटा के साथ जमा किए गए फॉर्म स्वीकार नहीं किए जाएंगे। इसके बजाय, फ़ॉर्म को फिर से सुधार के लिए उपयोगकर्ता को प्रस्तुत किया जाएगा। यह वही है जो हमें चाहिए।

हमने जो मिस किया था वह उपयोगकर्ता के नोटिफिकेशन था कि फॉर्म में क्या गलत था। सौभाग्य से, Flask-WTF भी इस कार्य को आसान बनाता है।

जब कोई क्षेत्र सत्यापन विफल होता है Flask-WTF फॉर्म ऑब्जेक्ट में एक दृश्य त्रुटि संदेश जोड़ता है। ये संदेश टेम्पलेट में उपलब्ध हैं, इसलिए हमें उन्हें प्रदर्शित करने के लिए कुछ तर्क जोड़ने की आवश्यकता है।

यह क्षेत्र सत्यापन संदेशों के साथ हमारा लॉगिन टेम्प्लेट है (फ़ाइल ऐप / टेम्प्लेट / लॉगिन.html):
 <!-- extend base layout --> {% extends "base.html" %} {% block content %} <h1>Sign In</h1> <form action="" method="post" name="login"> {{form.hidden_tag()}} <p> Please enter your OpenID:<br> {{form.openid(size=80)}}<br> {% for error in form.errors.openid %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form> {% endblock %} 


एकमात्र बदलाव जो हमने किया था वह था कि ओपनर फ़ील्ड के दाईं ओर सत्यापनकर्ता द्वारा जोड़े गए किसी भी त्रुटि संदेश को लूप ड्रॉ करना। एक नियम के रूप में, संलग्न सत्यापनकर्ताओं वाले किसी भी फ़ील्ड में form.errors._ रूप में त्रुटियों को जोड़ा जाएगा। हमारे मामले में, हम form.errors.openid उपयोग form.errors.openid । हम उपयोगकर्ता को उनका ध्यान आकर्षित करने के लिए इन संदेशों को लाल रंग में प्रदर्शित करते हैं।

OpenID के साथ सहभागिता


वास्तव में, हम इस तथ्य का सामना करेंगे कि बहुत से लोग यह भी नहीं जानते हैं कि उनके पास पहले से ही ओपनआईडी का एक जोड़ा है। यह अच्छी तरह से ज्ञात नहीं है कि कई प्रमुख इंटरनेट सेवा प्रदाता अपने उपयोगकर्ताओं के लिए OpenID प्रमाणीकरण का समर्थन करते हैं। उदाहरण के लिए, यदि आपके पास Google खाता है, तो आपके पास इसके साथ OpenID है। साथ ही याहू, एओएल, फ्लिकर और कई अन्य सेवाओं में।

अक्सर उपयोग किए जाने वाले ओपनआईडी में से एक के साथ हमारी साइट तक उपयोगकर्ता की पहुंच को सुविधाजनक बनाने के लिए, हम उनमें से कुछ से लिंक जोड़ देंगे ताकि उपयोगकर्ता को मैन्युअल रूप से ओपनआईडी दर्ज करने की आवश्यकता न हो।

आइए उन OpenID प्रदाताओं की सूची को परिभाषित करके शुरू करें जिन्हें हम पेश करना चाहते हैं। हम अपनी कॉन्फ़िगरेशन फ़ाइल (config.py file) में ऐसा कर सकते हैं:
 OPENID_PROVIDERS = [ { 'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id' }, { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' }, { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' }, { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }] 


अब देखते हैं कि हम अपने लॉगिन दृश्य फ़ंक्शन में इस सूची का उपयोग कैसे करते हैं:
 @app.route('/login', methods = ['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data)) return redirect('/index') return render_template('login.html', title = 'Sign In', form = form, providers = app.config['OPENID_PROVIDERS']) 


यहाँ हम app.config में कुंजी द्वारा उन्हें खोज कर सेटिंग्स प्राप्त करते हैं। फिर सूची को टेम्प्लेट तर्क के रूप में render_template कॉल में जोड़ा जाता है।

जैसा कि आपने अनुमान लगाया होगा, हमें इसे समाप्त करने के लिए एक और कदम उठाने की आवश्यकता है। अब हमें यह निर्दिष्ट करने की आवश्यकता है कि हम अपने लॉगिन टेम्प्लेट में इन प्रदाताओं के लिंक कैसे प्रदर्शित करना चाहते हैं (फ़ाइल ऐप / टेम्प्लेट / लॉगिन। Html):
 <!-- extend base layout --> {% extends "base.html" %} {% block content %} <script type="text/javascript"> function set_openid(openid, pr) { u = openid.search('<username>') if (u != -1) { // openid requires username user = prompt('Enter your ' + pr + ' username:') openid = openid.substr(0, u) + user } form = document.forms['login']; form.elements['openid'].value = openid } </script> <h1>Sign In</h1> <form action="" method="post" name="login"> {{form.hidden_tag()}} <p> Please enter your OpenID, or select one of the providers below:<br> {{form.openid(size=80)}} {% for error in form.errors.openid %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> |{% for pr in providers %} <a href="javascript:set_openid('{{pr.url}}', '{{pr.name}}');">{{pr.name}}</a> | {% endfor %} </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form> {% endblock %} 


इन सभी परिवर्तनों के संबंध में टेम्प्लेट कुछ लंबा निकला। कुछ OpenID में उपयोगकर्ता नाम शामिल हैं, उनके लिए हमारे पास कुछ जावास्क्रिप्ट जादू होना चाहिए जो एक उपयोगकर्ता नाम का अनुरोध करता है और फिर एक OpenID बनाता है। जब कोई उपयोगकर्ता प्रदाता के OpenID लिंक पर क्लिक करता है और (वैकल्पिक रूप से) उपयोगकर्ता नाम में प्रवेश करता है, तो इस प्रदाता के लिए OpenID को पाठ बॉक्स में डाला जाता है।


Google OpenID लिंक पर क्लिक करने के बाद हमारे लॉगिन पृष्ठ का स्क्रीनशॉट

अंतिम शब्द


यद्यपि हमने अपने लॉगिन रूपों के साथ बहुत प्रगति की है, लेकिन वास्तव में हमने उपयोगकर्ताओं को अपने सिस्टम में प्रवेश करने के लिए कुछ भी नहीं किया है। हमने जो भी किया वह लॉगिन प्रक्रिया के जीयूआई से संबंधित था। ऐसा इसलिए है क्योंकि इससे पहले कि हम वास्तविक लॉगिन कर सकें, हमें एक डेटाबेस रखना होगा जहाँ हम अपने उपयोगकर्ताओं को रिकॉर्ड कर सकें।

अगले भाग में, हम अपने डेटाबेस को बढ़ाएँगे और लॉन्च करेंगे, थोड़ी देर बाद हम अपना लॉगिन सिस्टम पूरा करेंगे, इसलिए निम्नलिखित लेखों के अपडेट के लिए हमारे साथ बने रहें।

इसकी वर्तमान स्थिति में माइक्रोब्लॉग एप्लिकेशन यहां डाउनलोड के लिए उपलब्ध है:
microblog-0.3.zip

ध्यान दें कि ज़िप फ़ाइल में फ्लास्क वर्चुअल वातावरण नहीं है। आवेदन शुरू करने से पहले, पहले भाग में दिए गए निर्देशों का पालन करते हुए, इसे स्वयं बनाएं।

यदि आपके पास कोई प्रश्न या टिप्पणी है, तो स्वतंत्र रूप से उन्हें नीचे छोड़ दें।

मिगुएल

Source: https://habr.com/ru/post/In194062/


All Articles